Skip to content
59 changes: 58 additions & 1 deletion cli/Valet/CommandLine.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,63 @@ public function shellExec($command) {
return shell_exec($command);
}

/**
* Stream command output in real time and optionally collect matching lines.
*
* @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)) {
Comment on lines +50 to +51
$line = fgets($handle);
if ($line === false) {
break;
}

// 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 {
output($line, false);
}

// 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)) {
$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.
*
Expand Down Expand Up @@ -151,4 +208,4 @@ public function runCommand($command, ?callable $onError = null, $realTimeOutput
return new ProcessOutput($process);
}
}
}
}
76 changes: 62 additions & 14 deletions cli/Valet/ShareTools/Ngrok.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,31 @@ 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: <fg=blue>https://ngrok.com/signup</>
Then use: <fg=magenta>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: <fg=blue>https://ngrok.com/signup</>\nThen use: <fg=magenta>valet set-ngrok-token [token]</>");
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
//
// (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'
];

// 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);
Expand All @@ -41,16 +56,49 @@ 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`");

$output = $this->cli->shellExec("$ngrokCommand 2>&1");
// 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.
$isErrorLine = function ($line) {
return strpos($line, 'ERROR:') !== false;
};
Comment on lines +68 to +70

$didOutputShareUrl = false;

if ($errors = strstr($output, "ERROR")) {
error($errors . PHP_EOL);
// 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: <fg=blue>$matches[1]</>");

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");
// Copy the public URL to the clipboard for ease.
$this->copyUrlToClipboard($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
]);

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");
}
}

Expand Down Expand Up @@ -124,4 +172,4 @@ protected function hasAuthToken(): bool {
}
return false;
}
}
}
13 changes: 11 additions & 2 deletions cli/Valet/ShareTools/ShareTool.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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.");
Comment on lines +141 to +142
}
}
22 changes: 17 additions & 5 deletions cli/includes/helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down Expand Up @@ -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);
}
Expand All @@ -78,25 +84,31 @@ function error(string $output, $exception = false) {
usleep(1);

// Print the error message to the console.
(new ConsoleOutput())->getErrorOutput()->writeln("\n\n<error>$errors</error>");
$errorOutput->write("\n\n<error>$errors</error>", $newline);

exit();
}
else {
(new ConsoleOutput())->getErrorOutput()->writeln("<error>$output</error>");
// If escapeOutput is true, then escape the output to prevent any formatting issues.
if ($escapeOutput) {
$output = OutputFormatter::escape($output);
}

$errorOutput->write("<error>$output</error>", $newline);
}
}

/**
* 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);
}

/**
Expand Down
1 change: 0 additions & 1 deletion cli/valet.php
Original file line number Diff line number Diff line change
Expand Up @@ -978,7 +978,6 @@

$url = Share::shareTool()->currentTunnelUrl($site);
info("The public URL for $site is: <fg=blue>$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"
Expand Down