#!/usr/bin/env php repository(); $comments = $app->comments(); $config = $app->config(); $stats = $app->stats(); $argv = $_SERVER['argv'] ?? []; array_shift($argv); $command = array_shift($argv) ?? 'help'; try { match ($command) { 'help', '--help', '-h' => help(), 'init' => init($repo), 'rebuild' => rebuild($repo), 'list' => listContent($repo, options($argv)), 'create' => createContent($repo, $argv), 'update' => updateContent($repo, $argv), 'delete' => deleteContent($repo, $argv), 'comments:list' => listComments($comments, options($argv)), 'comments:approve' => setCommentStatus($comments, $argv, 'approved'), 'comments:pending' => setCommentStatus($comments, $argv, 'pending'), 'comments:spam' => setCommentStatus($comments, $argv, 'spam'), 'comments:delete' => deleteComment($comments, $argv), 'site:set' => setConfig($config, $argv), 'social:list' => listSocial($config), 'social:add' => addSocial($config, $argv), 'stats:summary' => statsSummary($stats), default => fail('Unknown command "' . $command . '". Run php cli/blog.php help.'), }; } catch (Throwable $error) { fail($error->getMessage()); } function help(): void { echo <<rebuildIndex(); line('Content index rebuilt.'); } function rebuild(ContentRepository $repo): void { $repo->rebuildIndex(); line('Content index rebuilt.'); } /** @param array $opts */ function listContent(ContentRepository $repo, array $opts): void { $includeDrafts = isset($opts['all']); $type = (string) ($opts['type'] ?? 'post'); $items = $type === 'page' ? $repo->pages($includeDrafts) : $repo->posts($includeDrafts); if ($items === []) { line('No content found.'); return; } foreach ($items as $item) { printf( "%-22s %-10s %-10s %s\n", $item['slug'], $item['type'], $item['status'], $item['title'] ); } } /** @param array $args */ function createContent(ContentRepository $repo, array $args): void { $title = array_shift($args); if ($title === null || str_starts_with($title, '--')) { fail('A title is required.'); } $opts = options($args); $slug = (string) ($opts['slug'] ?? ContentRepository::slugify($title)); $body = bodyFromOptions($opts, "# {$title}\n\nStart writing here."); $metadata = metadataFromOptions($opts, [ 'title' => $title, 'type' => $opts['type'] ?? 'post', 'status' => $opts['status'] ?? 'draft', ]); $item = $repo->save($slug, $metadata, $body); line('Saved ' . $item['type'] . ' "' . $item['title'] . '" at bl-content/pages/' . $item['slug'] . '/index.txt'); } /** @param array $args */ function updateContent(ContentRepository $repo, array $args): void { $slug = array_shift($args); if ($slug === null || str_starts_with($slug, '--')) { fail('A slug is required.'); } $item = $repo->find($slug, true); if ($item === null) { fail('No content exists for slug "' . $slug . '".'); } $opts = options($args); $body = bodyFromOptions($opts, (string) $item['markdown']); $metadata = metadataFromOptions($opts, $item); $newSlug = (string) ($opts['slug'] ?? $slug); $saved = $repo->save($newSlug, $metadata, $body); if ($newSlug !== $slug) { $repo->delete($slug); } line('Updated ' . $saved['slug'] . '.'); } /** @param array $args */ function deleteContent(ContentRepository $repo, array $args): void { $slug = $args[0] ?? ''; if ($slug === '') { fail('A slug is required.'); } line($repo->delete($slug) ? 'Deleted ' . $slug . '.' : 'No content exists for slug "' . $slug . '".'); } /** @param array $opts */ function listComments($comments, array $opts): void { $rows = $comments->list($opts['status'] ?? null, $opts['slug'] ?? null); if ($rows === []) { line('No comments found.'); return; } foreach ($rows as $row) { printf( "#%-4d %-9s %-20s %-18s %s\n", $row['id'], $row['status'], $row['slug'], $row['author'], mb_substr(str_replace(["\r", "\n"], ' ', $row['body']), 0, 64) ); } } /** @param array $args */ function setCommentStatus($comments, array $args, string $status): void { $id = (int) ($args[0] ?? 0); if ($id < 1) { fail('A numeric comment id is required.'); } line($comments->setStatus($id, $status) ? 'Comment updated.' : 'Comment not found.'); } /** @param array $args */ function deleteComment($comments, array $args): void { $id = (int) ($args[0] ?? 0); if ($id < 1) { fail('A numeric comment id is required.'); } line($comments->delete($id) ? 'Comment deleted.' : 'Comment not found.'); } /** @param array $args */ function setConfig($config, array $args): void { $key = $args[0] ?? ''; $value = $args[1] ?? null; if ($key === '' || $value === null) { fail('Usage: php cli/blog.php site:set site.title "My Blog"'); } $config->set($key, parseCliValue($value)); $config->save(); line('Updated config key ' . $key . '.'); } function listSocial($config): void { $social = $config->get('social', []); if (!is_array($social) || $social === []) { line('No social links configured.'); return; } foreach ($social as $network => $item) { printf("%-14s %-18s %s\n", $network, $item['label'] ?? $network, $item['url'] ?? ''); } } /** @param array $args */ function addSocial($config, array $args): void { [$network, $label, $url] = [$args[0] ?? '', $args[1] ?? '', $args[2] ?? '']; if ($network === '' || $label === '' || $url === '') { fail('Usage: php cli/blog.php social:add github "GitHub" https://github.com/example'); } $social = $config->get('social', []); $social[$network] = ['label' => $label, 'url' => $url]; $config->set('social', $social); $config->save(); line('Added social link ' . $network . '.'); } function statsSummary($stats): void { $summary = $stats->summary(); line('Visits: ' . $summary['visits']); line('Routes:'); foreach ($summary['routes'] as $route => $count) { line(' ' . $route . ': ' . $count); } line('Top paths:'); foreach ($summary['top_paths'] as $path => $count) { line(' ' . $path . ': ' . $count); } } /** @param array $args @return array */ function options(array $args): array { $options = []; foreach ($args as $arg) { if (!str_starts_with($arg, '--')) { continue; } $arg = substr($arg, 2); if (str_contains($arg, '=')) { [$key, $value] = explode('=', $arg, 2); $options[$key] = parseCliValue($value); } else { $options[$arg] = true; } } return $options; } /** @param array $opts @param array $base @return array */ function metadataFromOptions(array $opts, array $base): array { foreach (['title', 'slug', 'type', 'status', 'date', 'category', 'summary', 'author', 'cover', 'menu_order'] as $key) { if (array_key_exists($key, $opts)) { $base[$key] = $opts[$key]; } } if (array_key_exists('tags', $opts)) { $base['tags'] = ContentRepository::normalizeList($opts['tags']); } if (isset($opts['no-comments'])) { $base['allow_comments'] = false; } return $base; } /** @param array $opts */ function bodyFromOptions(array $opts, string $fallback): string { if (!isset($opts['body-file'])) { return $fallback; } $path = (string) $opts['body-file']; if (!is_file($path)) { fail('Body file not found: ' . $path); } return (string) file_get_contents($path); } function parseCliValue(string $value): mixed { $lower = mb_strtolower($value); if (in_array($lower, ['true', 'false'], true)) { return $lower === 'true'; } if (is_numeric($value) && preg_match('/^\d+$/', $value)) { return (int) $value; } return $value; } function line(string $message): void { echo $message . PHP_EOL; } function fail(string $message): never { fwrite(STDERR, $message . PHP_EOL); exit(1); }