'Run Kirby dev server and Tailwind watcher', 'command' => static function ($cli): void { $cwd = getcwd(); // Resolve paths $router = $cwd . '/vendor/getkirby/cms/router.php'; $docroot = $cli->arg('docroot') ?? 'public'; $docrootPath = $cwd . '/' . $docroot; // Basic checks if (!is_file($router)) { $cli->error('Kirby router not found. Make sure dependencies are installed: vendor/getkirby/cms/router.php'); return; } if (!is_dir($docrootPath)) { $cli->error("Document root not found: {$docrootPath}"); return; } // Helper function to check if a port is available $isPortAvailable = function (string $host, int $port): bool { $socket = @fsockopen($host, $port, $errno, $errstr, 1); if ($socket) { fclose($socket); return false; // Port is occupied } return true; // Port is available }; $host = 'localhost'; $startPort = 8000; $maxPort = 8100; $port = $startPort; while ($port <= $maxPort) { if ($isPortAvailable($host, $port)) { break; } $port++; } if ($port > $maxPort) { $cli->error("No available ports found between {$startPort} and {$maxPort}"); return; } $cli->info("Using port {$port} for PHP dev server"); $phpCmd = sprintf( 'php -S %s:%s -t %s %s', escapeshellarg($host), escapeshellarg($port), escapeshellarg($docrootPath), escapeshellarg($router) ); $cssCmd = 'npm run watch:css'; $jsCmd = 'npm run watch:js'; $cli->info("Starting PHP dev server on http://{$host}:{$port} (docroot: {$docroot})"); // Helper to start a process with pipes $start = static function (string $cmd) use ($cwd) { $descriptorSpec = [ 0 => ['pipe', 'r'], // stdin 1 => ['pipe', 'w'], // stdout 2 => ['pipe', 'w'], // stderr ]; $proc = proc_open($cmd, $descriptorSpec, $pipes, $cwd); if (!is_resource($proc)) { return null; } // Non-blocking I/O stream_set_blocking($pipes[1], false); stream_set_blocking($pipes[2], false); return [ 'proc' => $proc, 'pipes' => $pipes, 'cmd' => $cmd, ]; }; $processes = []; $phpProc = $start($phpCmd); if ($phpProc === null) { $cli->error('Failed to start PHP dev server.'); return; } $processes['php'] = $phpProc; $npmProc = $start($cssCmd); if ($npmProc === null) { $cli->warning('Could not start npm watcher. Is Node/npm installed and in your PATH?'); } else { $processes['css'] = $npmProc; } $jsProc = $start($jsCmd); if ($jsProc === null) { $cli->warning('Could not start npm watcher. Is Node/npm installed and in your PATH?'); } else { $processes['js'] = $jsProc; } // Graceful termination $terminateAll = static function () use (&$processes, $cli): void { $cli->line(""); foreach ($processes as $name => $p) { // Check if the resource is still valid before calling proc_get_status if (is_resource($p['proc'])) { $status = proc_get_status($p['proc']); if ($status && $status['running']) { $cli->line("Stopping {$name}..."); // Try graceful terminate @proc_terminate($p['proc']); } } // Close pipes regardless of process status foreach ($p['pipes'] as $pipe) { if (is_resource($pipe)) { @fclose($pipe); } } // Final close if the resource is still valid if (is_resource($p['proc'])) { @proc_close($p['proc']); } } }; // Handle Ctrl+C if pcntl is available if (function_exists('pcntl_async_signals') && function_exists('pcntl_signal')) { pcntl_async_signals(true); pcntl_signal(SIGINT, static function () use ($terminateAll) { $terminateAll(); // Exit cleanly after terminating children exit(130); }); // Optional: also handle SIGTERM pcntl_signal(SIGTERM, static function () use ($terminateAll) { $terminateAll(); exit(143); }); } // Ensure cleanup on normal shutdown register_shutdown_function($terminateAll); // Stream outputs until all children exit $prefixColor = [ 'php' => fn($s) => "\033[1;34m [php]\033[0m $s", 'css' => fn($s) => "\033[1;32m󱏿 [css]\033[0m $s", 'js' => fn($s) => "\033[1;97m [js]\033[0m $s", ]; while (true) { $running = false; foreach ($processes as $name => $p) { // Check if the resource is still valid if (!is_resource($p['proc'])) { continue; } $status = proc_get_status($p['proc']); if (!$status) { continue; } $running = $running || $status['running']; // Read stdout if (is_resource($p['pipes'][1])) { $out = @stream_get_contents($p['pipes'][1]); if ($out !== false && $out !== '') { foreach (preg_split('/\R/', rtrim($out)) as $line) { if ($line !== '') { $cli->line(($prefixColor[$name])($line)); } } } } // Read stderr if (is_resource($p['pipes'][2])) { $err = @stream_get_contents($p['pipes'][2]); if ($err !== false && $err !== '') { foreach (preg_split('/\R/', rtrim($err)) as $line) { if ($line !== '') { $cli->error(($prefixColor[$name])($line)); } } } } } if (!$running) { break; } // Small sleep to avoid busy-waiting usleep(50_000); } $cli->success('All processes exited.'); } ];