feat(kirby): added serve command for kirby cli

This commit is contained in:
Stephan Plöhn 2025-08-26 10:20:24 +02:00
parent 315f7e9896
commit f3b60adb21
2 changed files with 211 additions and 2 deletions

View file

@ -6,7 +6,7 @@
"kirby", "kirby",
"cms", "cms",
"starterkit", "starterkit",
"tailwindcss "tailwindcss"
], ],
"authors": [ "authors": [
{ {
@ -23,7 +23,8 @@
}, },
"require": { "require": {
"php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0",
"getkirby/cms": "^5.0" "getkirby/cms": "^5.0",
"ext-pcntl": "*"
}, },
"config": { "config": {
"allow-plugins": { "allow-plugins": {

208
site/commands/serve.php Normal file
View file

@ -0,0 +1,208 @@
<?php
return [
'description' => '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)
);
$npmCmd = 'npm run watch';
$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($npmCmd);
if ($npmProc === null) {
$cli->warning('Could not start npm watcher. Is Node/npm installed and in your PATH?');
} else {
$processes['npm'] = $npmProc;
}
// 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",
'npm' => fn($s) => "\033[1;32m [npm]\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.');
}
];