From c213254b5151a8a3406a40df2eace5152b9d8478 Mon Sep 17 00:00:00 2001 From: yCodeTech Date: Fri, 19 Jun 2026 00:03:34 +0100 Subject: [PATCH 1/8] fix: ngrok error logging by adding real-time output It was really hard to figure out if there was something wrong with ngrok without proper logging. Ngrok does actually have a --log option. so we can use this to stream live logging output directly to the terminal. - Implemented `streamCommandOutput` method in `CommandLine` to handle command output in real time, while also optionally collecting errors for later checks, and format errors as valet errors. - Enhanced `Ngrok` class to utilize the new streaming method with the --log options for better error handling and output visibility. - Updated `error` helper function to support additional `newline` and `escapeOutput` parameters for output formatting, and changed the Symfony's `writeln` to `write` so we can toggle on/off the writing of a newline at the end of the output. --- cli/Valet/CommandLine.php | 46 ++++++++++++++++++++++++++++++++++ cli/Valet/ShareTools/Ngrok.php | 21 ++++++++++------ cli/includes/helpers.php | 19 +++++++++++--- 3 files changed, 75 insertions(+), 11 deletions(-) diff --git a/cli/Valet/CommandLine.php b/cli/Valet/CommandLine.php index f719867a5..c7eeb3e77 100644 --- a/cli/Valet/CommandLine.php +++ b/cli/Valet/CommandLine.php @@ -27,6 +27,52 @@ public function shellExec($command) { return shell_exec($command); } + /** + * Stream command output in real time and optionally collect matching lines. + * + * @param string $command + * @param callable|null $lineMatches Callback to check matching lines; must return `true` + * to collect the line for post-run analysis. + * @param callable|null $lineIsError Callback to check whether a line should be + * rendered as an error in real time. If omitted, the capture matcher is reused. + * + * @return array The collected output lines or an empty array if no lines were collected. + */ + public function streamCommandOutput($command, ?callable $lineMatches = null, ?callable $lineIsError = null): array { + $capturedLines = []; + $lineIsError = $lineIsError ?: $lineMatches; + + // Open a process to execute the command and read its output. + $handle = popen("$command 2>&1", 'r'); + while ($handle && !feof($handle)) { + $line = fgets($handle); + if ($line === false) { + break; + } + + // Keep raw command output unless caller explicitly marks this line as an error. + if ($lineIsError && $lineIsError($line)) { + error($line, false, false, true); + } + else { + echo $line; + } + + // If a callback is provided and the line matches the condition, + // then collect the line for post-run analysis. + if ($lineMatches && $lineMatches($line)) { + $capturedLines[] = trim($line); + } + } + + // Close the process. + if ($handle) { + pclose($handle); + } + + return $capturedLines; + } + /** * Pass the given Valet command to the command line with elevated privileges using gsudo. * diff --git a/cli/Valet/ShareTools/Ngrok.php b/cli/Valet/ShareTools/Ngrok.php index 010cecba0..0c98e205b 100644 --- a/cli/Valet/ShareTools/Ngrok.php +++ b/cli/Valet/ShareTools/Ngrok.php @@ -38,19 +38,26 @@ public function start(string $site, int $port, array $options = []) { $ngrok = realpath(valetBinPath() . 'ngrok.exe'); - $ngrokCommand = "\"$ngrok\" http $site:$port " . $this->getConfig() . " $options"; + // Log to stdout, log level info, and log format term for real-time output. + $logging = "--log=stdout --log-level=info --log-format=term"; + + $ngrokCommand = "\"$ngrok\" http $site:$port " . $this->getConfig() . " $options $logging"; info("Sharing $site...\n"); info("To output the public URL, please open a new terminal and run `valet fetch-share-url $site`"); - $output = $this->cli->shellExec("$ngrokCommand 2>&1"); + // Stream ngrok output in real time and collect error lines for post-run analysis. + // Shared matcher: use the same rule for live error styling and for post-run capture. + $isErrorLine = function ($line) { + return strpos($line, 'ERROR:') !== false; + }; - if ($errors = strstr($output, "ERROR")) { - error($errors . PHP_EOL); + // Pass the same matcher once; CommandLine reuses it for error styling when no separate + // error callback is supplied. + $errorLines = $this->cli->streamCommandOutput($ngrokCommand, $isErrorLine); - if (strpos($errors, 'ERR_NGROK_121') !== false) { - info("To update ngrok yourself, please run `valet ngrok update` and then upgrade the config file by running `valet ngrok config upgrade`\n"); - } + if (!empty($errorLines) && strpos(implode("\n", $errorLines), 'ERR_NGROK_121') !== false) { + info("\nTo update ngrok yourself, please run `valet ngrok update` and then upgrade the config file by running `valet ngrok config upgrade`\n"); } } diff --git a/cli/includes/helpers.php b/cli/includes/helpers.php index 667780c45..93f686e32 100644 --- a/cli/includes/helpers.php +++ b/cli/includes/helpers.php @@ -11,6 +11,7 @@ use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Formatter\OutputFormatter; if (!isset($_SERVER['HOME'])) { $_SERVER['HOME'] = $_SERVER['USERPROFILE']; @@ -62,11 +63,16 @@ function warning($output) { * * @param string $output * @param bool $exception Optionally pass a boolean to indicate whether to throw an exception. If `true`, the error will be thrown as a `ValetException`. [default: `false`] + * @param bool $newline Whether to append a newline after the error output. [default: `true`] + * @param bool $escapeOutput Whether to escape the output to prevent formatting issues. [default: `false`] * * @throws RuntimeException * @throws ValetException */ -function error(string $output, $exception = false) { +function error(string $output, bool $exception = false, bool $newline = true, bool $escapeOutput = false) { + + $errorOutput = (new ConsoleOutput())->getErrorOutput(); + if (isset($_ENV['APP_ENV']) && $_ENV['APP_ENV'] === 'testing') { throw new RuntimeException($output); } @@ -78,12 +84,17 @@ function error(string $output, $exception = false) { usleep(1); // Print the error message to the console. - (new ConsoleOutput())->getErrorOutput()->writeln("\n\n$errors"); + $errorOutput->write("\n\n$errors", $newline); exit(); } else { - (new ConsoleOutput())->getErrorOutput()->writeln("$output"); + // If escapeOutput is true, then escape the output to prevent any formatting issues. + if ($escapeOutput) { + $output = OutputFormatter::escape($output); + } + + $errorOutput->write("$output", $newline); } } @@ -385,4 +396,4 @@ function str_contains_any($haystack, $needles) { } } return false; -} +} \ No newline at end of file From 49cad724d5dc8d0110e987d37bf987bbb0c918a7 Mon Sep 17 00:00:00 2001 From: yCodeTech Date: Sat, 27 Jun 2026 20:59:10 +0100 Subject: [PATCH 2/8] fix: improve ngrok error message formatting & add newline characters for clarity --- cli/Valet/ShareTools/Ngrok.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cli/Valet/ShareTools/Ngrok.php b/cli/Valet/ShareTools/Ngrok.php index 0c98e205b..212972153 100644 --- a/cli/Valet/ShareTools/Ngrok.php +++ b/cli/Valet/ShareTools/Ngrok.php @@ -22,9 +22,7 @@ class Ngrok extends ShareTool { */ public function start(string $site, int $port, array $options = []) { if ($port === 443 && !$this->hasAuthToken()) { - output('Forwarding to local port 443 or a local https:// URL is only available after you sign up. -Sign up at: https://ngrok.com/signup -Then use: valet set-ngrok-token [token]'); + output("Forwarding to local port 443 or a local https:// URL is only available after you sign up.\nSign up at: https://ngrok.com/signup\nThen use: valet set-ngrok-token [token]"); exit(1); } From 5faee51ac28192fcc0f7332eeec354e6cbcca355 Mon Sep 17 00:00:00 2001 From: yCodeTech Date: Sun, 28 Jun 2026 01:04:16 +0100 Subject: [PATCH 3/8] refactor: `output` helper function to toggle newlines on/off. - Updated `output` helper function to accept a `newline` parameter, and changed the Symfony's `writeln` to `write` so we can toggle on/off the writing of a newline at the end of the output. --- cli/includes/helpers.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cli/includes/helpers.php b/cli/includes/helpers.php index 93f686e32..1f8d5ee1f 100644 --- a/cli/includes/helpers.php +++ b/cli/includes/helpers.php @@ -102,12 +102,13 @@ function error(string $output, bool $exception = false, bool $newline = true, bo * Output the given text to the console. * * @param string $output + * @param bool $newline Whether to append a newline after the output. [default: `true`] */ -function output($output) { +function output($output, bool $newline = true) { if (isset($_ENV['APP_ENV']) && $_ENV['APP_ENV'] === 'testing') { return; } - (new ConsoleOutput())->writeln($output); + (new ConsoleOutput())->write($output, $newline); } /** @@ -396,4 +397,4 @@ function str_contains_any($haystack, $needles) { } } return false; -} \ No newline at end of file +} From 3f6af7dc85dacfbeab1e385642cc296841a18a35 Mon Sep 17 00:00:00 2001 From: yCodeTech Date: Sun, 28 Jun 2026 02:53:03 +0100 Subject: [PATCH 4/8] refactor: callback params in `streamCommandOutput` method to an array. - Refactored `CommandLine::streamCommandOutput` method: - Removed the callback `lineMatches` and `lineIsError` params. - Added a `callbacks` array param to define the many callbacks that may be used, for better flexibility. - Updated the callback defaults. - Updated ngrok integration to utilize the new callback structure for error line handling. --- cli/Valet/CommandLine.php | 13 +++++++------ cli/Valet/ShareTools/Ngrok.php | 7 ++++--- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/cli/Valet/CommandLine.php b/cli/Valet/CommandLine.php index c7eeb3e77..a2775a1b7 100644 --- a/cli/Valet/CommandLine.php +++ b/cli/Valet/CommandLine.php @@ -31,16 +31,17 @@ public function shellExec($command) { * Stream command output in real time and optionally collect matching lines. * * @param string $command - * @param callable|null $lineMatches Callback to check matching lines; must return `true` - * to collect the line for post-run analysis. - * @param callable|null $lineIsError Callback to check whether a line should be - * rendered as an error in real time. If omitted, the capture matcher is reused. + * @param array $callbacks Optional callbacks: + * - matches (callable): return true to collect line for post-run analysis. + * - isError (callable): return true to render line as an error. Defaults to matches. * * @return array The collected output lines or an empty array if no lines were collected. */ - public function streamCommandOutput($command, ?callable $lineMatches = null, ?callable $lineIsError = null): array { + public function streamCommandOutput($command, array $callbacks = []): array { + $lineMatches = $callbacks['matches'] ?? null; + $lineIsError = $callbacks['isError'] ?? $lineMatches; + $capturedLines = []; - $lineIsError = $lineIsError ?: $lineMatches; // Open a process to execute the command and read its output. $handle = popen("$command 2>&1", 'r'); diff --git a/cli/Valet/ShareTools/Ngrok.php b/cli/Valet/ShareTools/Ngrok.php index 212972153..a52301f8c 100644 --- a/cli/Valet/ShareTools/Ngrok.php +++ b/cli/Valet/ShareTools/Ngrok.php @@ -50,9 +50,10 @@ public function start(string $site, int $port, array $options = []) { return strpos($line, 'ERROR:') !== false; }; - // Pass the same matcher once; CommandLine reuses it for error styling when no separate - // error callback is supplied. - $errorLines = $this->cli->streamCommandOutput($ngrokCommand, $isErrorLine); + // Stream ngrok output in real time and collect error lines for post-run analysis. + $errorLines = $this->cli->streamCommandOutput($ngrokCommand, [ + 'matches' => $isErrorLine + ]); if (!empty($errorLines) && strpos(implode("\n", $errorLines), 'ERR_NGROK_121') !== false) { info("\nTo update ngrok yourself, please run `valet ngrok update` and then upgrade the config file by running `valet ngrok config upgrade`\n"); From 82288052455b10e2594f58f51671357320802279 Mon Sep 17 00:00:00 2001 From: yCodeTech Date: Sun, 28 Jun 2026 15:57:59 +0100 Subject: [PATCH 5/8] fix: enhance ngrok default options handling when they're not set by users - Refactored the host-header options conditional check in favour of defining the default options including the log options and their values in an array, and looping through them only adding them if they're not in the user options array. - Allowed the logging options to be overridden by users by removing the hardcoded logging options from the ngrok command in favour of the options array. --- cli/Valet/ShareTools/Ngrok.php | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/cli/Valet/ShareTools/Ngrok.php b/cli/Valet/ShareTools/Ngrok.php index a52301f8c..cda9d27fd 100644 --- a/cli/Valet/ShareTools/Ngrok.php +++ b/cli/Valet/ShareTools/Ngrok.php @@ -26,20 +26,31 @@ public function start(string $site, int $port, array $options = []) { exit(1); } - // If host-header is not specified, - // then set it into the array with a default value of rewrite. - if (!stripos(json_encode($options), 'host-header')) { - array_push($options, "host-header=rewrite"); + // Apply defaults for various options the user has not already specified. + $defaults = [ + 'host-header' => 'rewrite', + // Logging options: log to stdout at info level, enables real-time output + // and post-run error analysis. + // Logging options are undocumented for the http command, but is defined as + // API flags but still works for the http command. See ngrok docs for more details: + // https://ngrok.com/docs/agent/cli-api#flags-2 + 'log' => 'stdout', + 'log-level' => 'info', + 'log-format' => 'term' + ]; + + // Merge defaults with user-specified options, giving precedence to user-specified options. + foreach ($defaults as $key => $value) { + if (!array_filter($options, fn($opt) => strpos($opt, "$key=") === 0)) { + $options[] = "$key=$value"; + } } $options = prefixOptions($options); $ngrok = realpath(valetBinPath() . 'ngrok.exe'); - // Log to stdout, log level info, and log format term for real-time output. - $logging = "--log=stdout --log-level=info --log-format=term"; - - $ngrokCommand = "\"$ngrok\" http $site:$port " . $this->getConfig() . " $options $logging"; + $ngrokCommand = "\"$ngrok\" http $site:$port " . $this->getConfig() . " $options"; info("Sharing $site...\n"); info("To output the public URL, please open a new terminal and run `valet fetch-share-url $site`"); From 476dcc0253b26a9f16fc36a57c24e6fb0ffa8f0a Mon Sep 17 00:00:00 2001 From: yCodeTech Date: Sun, 28 Jun 2026 23:15:58 +0100 Subject: [PATCH 6/8] feat: enhance ngrok output handling to extract the public URL for ease - Added a `lineHandler` callback option to the `CommandLine::streamCommandOutput` method, and allow it to be called after the line has been outputted to the terminal. - Added a line handler closure function in `Ngrok::start` method to find and extract the public URL when the tunnel starts and output it as a Valet info message, so the user can easily identify the URL without having to use the `fetch-share-url` valet command. - Added a conditional to detect if the options string doesn't contain the log stdout/stderr option, then it'll output the info message to inform users they can use the `fetch-share-url` valet command. This message doesn't need to be outputted if it is logging to stdout/stderr as it tells the use about the url anyway. - Also added a notes about why stdout and stderr values are the same - because `CommandLine::streamCommandOutput` method uses `2>&1` to redirect stderr to stdout. --- cli/Valet/CommandLine.php | 9 +++++++++ cli/Valet/ShareTools/Ngrok.php | 30 +++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/cli/Valet/CommandLine.php b/cli/Valet/CommandLine.php index a2775a1b7..42d501903 100644 --- a/cli/Valet/CommandLine.php +++ b/cli/Valet/CommandLine.php @@ -32,18 +32,21 @@ public function shellExec($command) { * * @param string $command * @param array $callbacks Optional callbacks: + * - onLine (callable): receives every raw line after it is written. * - matches (callable): return true to collect line for post-run analysis. * - isError (callable): return true to render line as an error. Defaults to matches. * * @return array The collected output lines or an empty array if no lines were collected. */ public function streamCommandOutput($command, array $callbacks = []): array { + $lineHandler = $callbacks['onLine'] ?? null; $lineMatches = $callbacks['matches'] ?? null; $lineIsError = $callbacks['isError'] ?? $lineMatches; $capturedLines = []; // Open a process to execute the command and read its output. + // 2>&1 redirects stderr to stdout so we can capture both. $handle = popen("$command 2>&1", 'r'); while ($handle && !feof($handle)) { $line = fgets($handle); @@ -59,6 +62,12 @@ public function streamCommandOutput($command, array $callbacks = []): array { echo $line; } + // Invoke the optional line handler after writing output so callers can append + // follow-up messages in display order. + if ($lineHandler) { + $lineHandler($line); + } + // If a callback is provided and the line matches the condition, // then collect the line for post-run analysis. if ($lineMatches && $lineMatches($line)) { diff --git a/cli/Valet/ShareTools/Ngrok.php b/cli/Valet/ShareTools/Ngrok.php index cda9d27fd..15f784c83 100644 --- a/cli/Valet/ShareTools/Ngrok.php +++ b/cli/Valet/ShareTools/Ngrok.php @@ -34,6 +34,9 @@ public function start(string $site, int $port, array $options = []) { // Logging options are undocumented for the http command, but is defined as // API flags but still works for the http command. See ngrok docs for more details: // https://ngrok.com/docs/agent/cli-api#flags-2 + // + // (Note: Both `stdout` and `stderr` values capture the same output since + // `CommandLine::streamCommandOutput` method uses `2>&1` to redirect stderr to stdout.) 'log' => 'stdout', 'log-level' => 'info', 'log-format' => 'term' @@ -53,7 +56,12 @@ public function start(string $site, int $port, array $options = []) { $ngrokCommand = "\"$ngrok\" http $site:$port " . $this->getConfig() . " $options"; info("Sharing $site...\n"); - info("To output the public URL, please open a new terminal and run `valet fetch-share-url $site`"); + + // If the options string doesn't contain the `--log` option with values of either `stdout` + // or `stderr`,then inform the user that they can fetch the public URL in a new terminal. + if (strpos($options, '--log=stdout') === false && strpos($options, '--log=stderr') === false) { + info("To output the public URL, please open a new terminal and run `valet fetch-share-url $site`"); + } // Stream ngrok output in real time and collect error lines for post-run analysis. // Shared matcher: use the same rule for live error styling and for post-run capture. @@ -61,8 +69,28 @@ public function start(string $site, int $port, array $options = []) { return strpos($line, 'ERROR:') !== false; }; + $didOutputShareUrl = false; + + // Line handler: check each line for the "started tunnel" log line to find and + // extract the public URL. + $lineHandler = function ($line) use ($site, &$didOutputShareUrl) { + // If the share URL has already been output, skip further processing. + if ($didOutputShareUrl) { + return; + } + + // If the line contains the 'msg="started tunnel"' message AND has a 'url=' key... + if (strpos($line, 'msg="started tunnel"') !== false && preg_match('/\burl=(\S+)/', $line, $matches)) { + // Set the flag to true to avoid further processing of lines. + $didOutputShareUrl = true; + // Output an info message with extracted public URL. + info("The public URL for $site is: $matches[1]"); + } + }; + // Stream ngrok output in real time and collect error lines for post-run analysis. $errorLines = $this->cli->streamCommandOutput($ngrokCommand, [ + 'onLine' => $lineHandler, 'matches' => $isErrorLine ]); From eea5b05121d0a5bf8a60dfa5256ee63a41c60499 Mon Sep 17 00:00:00 2001 From: yCodeTech Date: Sun, 28 Jun 2026 23:29:58 +0100 Subject: [PATCH 7/8] fix: outputting the URL info before the line is even outputted from ngrok ngrok's command output is now processed through the `output` helper function which uses Symfony's `ConsoleOutput` instead of relying on PHP's `echo`. This fixes the output race where the URL info in the lineHandler was outputted before ngrok's own output was shown. By changing it to our helper, everything goes through Symfony's buffer system and output them in order. --- cli/Valet/CommandLine.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cli/Valet/CommandLine.php b/cli/Valet/CommandLine.php index 42d501903..8c0d7320c 100644 --- a/cli/Valet/CommandLine.php +++ b/cli/Valet/CommandLine.php @@ -54,12 +54,13 @@ public function streamCommandOutput($command, array $callbacks = []): array { break; } - // Keep raw command output unless caller explicitly marks this line as an error. + // If the line is an error, output it as an error. if ($lineIsError && $lineIsError($line)) { error($line, false, false, true); } + // Otherwise, output it normally. else { - echo $line; + output($line, false); } // Invoke the optional line handler after writing output so callers can append @@ -207,4 +208,4 @@ public function runCommand($command, ?callable $onError = null, $realTimeOutput return new ProcessOutput($process); } } -} \ No newline at end of file +} From daa51e3ad0f4d1f7f8abe4e4ff73de5be6cde930 Mon Sep 17 00:00:00 2001 From: yCodeTech Date: Sun, 28 Jun 2026 23:37:20 +0100 Subject: [PATCH 8/8] feat: add clipboard functionality for public URL in ngrok sharing - Implemented `ShareTool::copyUrlToClipboard` method to copy the specified URL to the clipboard, and changed the `ShareTool::currentTunnelUrl` method to use this new method instead of duplicating the code. - Removed the info message from the `fetch-share-url` command, and added it into the new `ShareTool::copyUrlToClipboard` method. - Updated `Ngrok:start` method to use the new `ShareTool::copyUrlToClipboard` method for copying the URL in the `lineHandler`. --- cli/Valet/ShareTools/Ngrok.php | 5 ++++- cli/Valet/ShareTools/ShareTool.php | 13 +++++++++++-- cli/valet.php | 1 - 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/cli/Valet/ShareTools/Ngrok.php b/cli/Valet/ShareTools/Ngrok.php index 15f784c83..854c5cc14 100644 --- a/cli/Valet/ShareTools/Ngrok.php +++ b/cli/Valet/ShareTools/Ngrok.php @@ -85,6 +85,9 @@ public function start(string $site, int $port, array $options = []) { $didOutputShareUrl = true; // Output an info message with extracted public URL. info("The public URL for $site is: $matches[1]"); + + // Copy the public URL to the clipboard for ease. + $this->copyUrlToClipboard($matches[1]); } }; @@ -169,4 +172,4 @@ protected function hasAuthToken(): bool { } return false; } -} +} \ No newline at end of file diff --git a/cli/Valet/ShareTools/ShareTool.php b/cli/Valet/ShareTools/ShareTool.php index e29e914d0..b22afe991 100644 --- a/cli/Valet/ShareTools/ShareTool.php +++ b/cli/Valet/ShareTools/ShareTool.php @@ -90,8 +90,7 @@ public function currentTunnelUrl(string $site) { if (isset($body->tunnels) && count($body->tunnels) > 0) { // If the tunnel URL is NOT null, return the URL. if ($tunnelUrl = $this->findHttpTunnelUrl($body->tunnels, $site)) { - // Use | clip to copy the URL to the clipboard. - $this->cli->passthru("echo $tunnelUrl | clip"); + $this->copyUrlToClipboard($tunnelUrl); return $tunnelUrl; } @@ -132,4 +131,14 @@ public function findHttpTunnelUrl(array $tunnels, ?string $site = null) { } return null; } + + /** + * Copy the public URL to the clipboard. + * + * @param string $url The public URL to copy. + */ + public function copyUrlToClipboard(string $url) { + $this->cli->passthru("echo $url | clip"); + info("It has been copied to your clipboard."); + } } diff --git a/cli/valet.php b/cli/valet.php index 1a23e90aa..2c49eaf9b 100644 --- a/cli/valet.php +++ b/cli/valet.php @@ -978,7 +978,6 @@ $url = Share::shareTool()->currentTunnelUrl($site); info("The public URL for $site is: $url"); - info("It has been copied to your clipboard."); })->setAliases(["url"])->descriptions('Get and copy the public URL of the current working directory site that is currently being shared', [ "site" => "Optionally, specify a site"