333 lines
9.5 KiB
PHP
333 lines
9.5 KiB
PHP
#!/usr/bin/env php
|
|
<?php
|
|
declare(strict_types=1);
|
|
|
|
use NeonBlog\ContentRepository;
|
|
|
|
$app = require dirname(__DIR__) . '/app/bootstrap.php';
|
|
$repo = $app->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 <<<TEXT
|
|
Neon Blog CLI
|
|
|
|
Content:
|
|
php cli/blog.php init
|
|
php cli/blog.php list [--type=post|page] [--all]
|
|
php cli/blog.php create "Title" [--slug=custom] [--type=post|page] [--status=published|draft] [--category=Name] [--tags=a,b] [--summary=text] [--body-file=/path/file.md] [--no-comments]
|
|
php cli/blog.php update slug [--title=New] [--status=published|draft] [--category=Name] [--tags=a,b] [--summary=text] [--body-file=/path/file.md]
|
|
php cli/blog.php delete slug
|
|
php cli/blog.php rebuild
|
|
|
|
Comments:
|
|
php cli/blog.php comments:list [--status=pending|approved|spam] [--slug=post-slug]
|
|
php cli/blog.php comments:approve 12
|
|
php cli/blog.php comments:pending 12
|
|
php cli/blog.php comments:spam 12
|
|
php cli/blog.php comments:delete 12
|
|
|
|
Site:
|
|
php cli/blog.php site:set site.title "My Blog"
|
|
php cli/blog.php social:list
|
|
php cli/blog.php social:add github "GitHub" https://github.com/example
|
|
php cli/blog.php stats:summary
|
|
|
|
TEXT;
|
|
}
|
|
|
|
function init(ContentRepository $repo): void
|
|
{
|
|
$repo->rebuildIndex();
|
|
line('Content index rebuilt.');
|
|
}
|
|
|
|
function rebuild(ContentRepository $repo): void
|
|
{
|
|
$repo->rebuildIndex();
|
|
line('Content index rebuilt.');
|
|
}
|
|
|
|
/** @param array<string, mixed> $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<int, string> $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<int, string> $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<int, string> $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<string, mixed> $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<int, string> $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<int, string> $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<int, string> $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<int, string> $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<int, string> $args @return array<string, mixed> */
|
|
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<string, mixed> $opts @param array<string, mixed> $base @return array<string, mixed> */
|
|
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<string, mixed> $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);
|
|
}
|