blog-backup/scripts/blog.sh

445 lines
11 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
DEFAULT_BLOG_API_URL="https://blog.twisteddevices.com/api"
if [[ -f "$PROJECT_ROOT/.env.local" ]]; then
source "$PROJECT_ROOT/.env.local"
fi
export BLOG_API_URL="${BLOG_API_URL:-$DEFAULT_BLOG_API_URL}"
if [[ -z "${BLOG_MACHINE_TOKEN:-}" ]]; then
if [[ -n "${CRON_API_KEY:-}" ]]; then
export BLOG_MACHINE_TOKEN="$CRON_API_KEY"
fi
fi
source "${HOME}/.agents/skills/blog-backup/lib/blog.sh"
url_encode() {
jq -rn --arg value "${1:-}" '$value|@uri'
}
API_STATUS=""
API_BODY=""
api_call() {
local method="$1"
local endpoint="$2"
local payload="${3:-}"
local url="${BLOG_API_URL}${endpoint}"
local curl_args=(-sS -X "$method" "$url")
local tmp_body
local http_code
tmp_body="$(mktemp)"
if [[ -n "${BLOG_MACHINE_TOKEN:-}" ]]; then
curl_args+=(-H "x-api-key: ${BLOG_MACHINE_TOKEN}")
fi
if [[ -n "$payload" ]]; then
curl_args+=(-H "Content-Type: application/json" -d "$payload")
fi
if ! http_code="$(curl "${curl_args[@]}" -o "$tmp_body" -w "%{http_code}")"; then
rm -f "$tmp_body"
return 1
fi
API_STATUS="$http_code"
API_BODY="$(cat "$tmp_body")"
rm -f "$tmp_body"
return 0
}
is_json() {
jq -e . >/dev/null 2>&1 <<<"${1:-}"
}
is_http_success() {
local code="${1:-0}"
[[ "$code" =~ ^[0-9]+$ ]] && (( code >= 200 && code < 300 ))
}
print_api_error() {
local endpoint="$1"
echo "❌ API request failed: ${BLOG_API_URL}${endpoint} (HTTP ${API_STATUS:-unknown})" >&2
if is_json "$API_BODY"; then
local message
message="$(jq -r '.error // .message // empty' <<<"$API_BODY")"
if [[ -n "$message" ]]; then
echo "Error: $message" >&2
fi
echo "$API_BODY" | jq . >&2
return
fi
echo "Response (non-JSON):" >&2
printf '%s\n' "$API_BODY" | head -c 800 >&2
echo >&2
if [[ "${BLOG_API_URL}" == "https://blog.twisteddevices.com/api" && "$API_BODY" == *"This page could not be found."* ]]; then
echo "Hint: /api/articles is not deployed there yet. For local testing use BLOG_API_URL=http://localhost:3002/api" >&2
fi
}
usage() {
cat <<'EOF'
Blog Backup CLI
Usage: ./blog.sh <command> [args]
Environment:
BLOG_API_URL API base (default: https://blog.twisteddevices.com/api)
For local dev use: BLOG_API_URL=http://localhost:3002/api
BLOG_MACHINE_TOKEN Optional machine token (falls back to CRON_API_KEY from .env.local)
Modern article commands:
article-add [args] Add one article (recommended)
--title "..." Article title (required)
--summary "..." Article summary (required)
--url "https://..." Source URL (optional)
--date YYYY-MM-DD Digest date (default: today)
--published-at ISO8601 Published timestamp (optional)
--tags "a,b,c" Comma-separated tags (optional)
--json Output full JSON
article-list [args] List article rows
--date YYYY-MM-DD Filter by digest day
--tag TAG Filter by tag
--limit N Max rows (default: 50)
--json Output full JSON
article-delete <id> Delete one article by id
rss Print RSS feed URL
Legacy digest commands:
post [args] Create a bundled digest post (legacy)
list [args] List digests (legacy)
get <id> Get digest by ID
delete <id> Delete digest by ID
search <query> Search digests by content
status <date> Check if digest exists for date
health Check API health
EOF
}
handle_article_add() {
local TITLE=""
local SUMMARY=""
local SOURCE_URL=""
local DIGEST_DATE="$(date +%F)"
local PUBLISHED_AT=""
local TAGS=""
local JSON_OUTPUT=false
while [[ $# -gt 0 ]]; do
case "$1" in
--title) TITLE="${2:-}"; shift 2 ;;
--summary) SUMMARY="${2:-}"; shift 2 ;;
--url) SOURCE_URL="${2:-}"; shift 2 ;;
--date) DIGEST_DATE="${2:-}"; shift 2 ;;
--published-at) PUBLISHED_AT="${2:-}"; shift 2 ;;
--tags) TAGS="${2:-}"; shift 2 ;;
--json) JSON_OUTPUT=true; shift ;;
*) echo "Unknown option: $1" >&2; exit 1 ;;
esac
done
if [[ -z "$TITLE" || -z "$SUMMARY" ]]; then
echo "Usage: ./blog.sh article-add --title <title> --summary <summary> [--url <url>] [--date <YYYY-MM-DD>] [--tags <a,b,c>]" >&2
exit 1
fi
if [[ -z "${BLOG_MACHINE_TOKEN:-}" ]]; then
echo "Error: BLOG_MACHINE_TOKEN (or CRON_API_KEY in .env.local) is required for article-add." >&2
exit 1
fi
local payload
payload="$(jq -n \
--arg title "$TITLE" \
--arg summary "$SUMMARY" \
--arg sourceUrl "$SOURCE_URL" \
--arg digestDate "$DIGEST_DATE" \
--arg publishedAt "$PUBLISHED_AT" \
--arg tagsCsv "$TAGS" \
'{
title: $title,
summary: $summary,
sourceUrl: (if $sourceUrl == "" then null else $sourceUrl end),
digestDate: (if $digestDate == "" then null else $digestDate end),
publishedAt: (if $publishedAt == "" then null else $publishedAt end),
tags: (if $tagsCsv == "" then [] else ($tagsCsv | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))) end)
}')"
if ! api_call POST "/articles" "$payload"; then
echo "❌ Failed to reach API endpoint." >&2
exit 1
fi
if ! is_http_success "$API_STATUS"; then
print_api_error "/articles"
exit 1
fi
if [[ "$JSON_OUTPUT" == "true" ]]; then
if is_json "$API_BODY"; then
echo "$API_BODY" | jq .
else
echo "$API_BODY"
fi
return
fi
if ! is_json "$API_BODY"; then
echo "❌ API returned non-JSON success response." >&2
echo "$API_BODY" >&2
exit 1
fi
echo "$API_BODY" | jq -r '"✅ Article added: \(.article.id // "unknown") | \(.article.title // "unknown")"'
}
handle_article_list() {
local DATE=""
local TAG=""
local LIMIT=50
local JSON_OUTPUT=false
while [[ $# -gt 0 ]]; do
case "$1" in
--date) DATE="${2:-}"; shift 2 ;;
--tag) TAG="${2:-}"; shift 2 ;;
--limit) LIMIT="${2:-}"; shift 2 ;;
--json) JSON_OUTPUT=true; shift ;;
*) echo "Unknown option: $1" >&2; exit 1 ;;
esac
done
local endpoint="/articles?limit=${LIMIT}"
if [[ -n "$DATE" ]]; then
endpoint="${endpoint}&digestDate=$(url_encode "$DATE")"
fi
if [[ -n "$TAG" ]]; then
endpoint="${endpoint}&tag=$(url_encode "$TAG")"
fi
if ! api_call GET "$endpoint"; then
echo "❌ Failed to reach API endpoint." >&2
exit 1
fi
if ! is_http_success "$API_STATUS"; then
print_api_error "$endpoint"
exit 1
fi
if [[ "$JSON_OUTPUT" == "true" ]]; then
if is_json "$API_BODY"; then
echo "$API_BODY" | jq .
else
echo "$API_BODY"
fi
return
fi
if ! is_json "$API_BODY"; then
echo "❌ API returned non-JSON response." >&2
echo "$API_BODY" >&2
exit 1
fi
echo "$API_BODY" | jq -r '.articles[]? | "\(.id)\t\(.digestDate)\t\(.title)\t\(.sourceUrl // "-")"'
}
handle_article_delete() {
local ID="${1:-}"
if [[ -z "$ID" ]]; then
echo "Usage: ./blog.sh article-delete <id>" >&2
exit 1
fi
if [[ -z "${BLOG_MACHINE_TOKEN:-}" ]]; then
echo "Error: BLOG_MACHINE_TOKEN (or CRON_API_KEY in .env.local) is required for article-delete." >&2
exit 1
fi
local endpoint="/articles/${ID}"
if ! api_call DELETE "$endpoint"; then
echo "❌ Failed to reach API endpoint." >&2
exit 1
fi
if ! is_http_success "$API_STATUS"; then
print_api_error "$endpoint"
exit 1
fi
if is_json "$API_BODY"; then
echo "$API_BODY" | jq .
else
echo "$API_BODY"
fi
}
handle_rss() {
local base_url="${BLOG_API_URL%/api}"
echo "${base_url}/api/rss"
}
handle_post() {
local DATE=""
local CONTENT=""
local TAGS='["daily-digest"]'
while [[ $# -gt 0 ]]; do
case "$1" in
--date) DATE="$2"; shift 2 ;;
--content) CONTENT="$2"; shift 2 ;;
--tags) TAGS="$2"; shift 2 ;;
*) echo "Unknown option: $1" >&2; shift ;;
esac
done
if [[ -z "$DATE" ]]; then
echo "Usage: ./blog.sh post --date <YYYY-MM-DD> --content <content> [--tags <tags>]" >&2
exit 1
fi
if [[ -z "$CONTENT" ]]; then
echo "Error: --content is required" >&2
exit 1
fi
local DIGEST_ID
DIGEST_ID=$(blog_post_create --date "$DATE" --content "$CONTENT" --tags "$TAGS")
if [[ $? -eq 0 ]]; then
echo "✅ Digest created: $DIGEST_ID"
else
echo "❌ Failed to create digest" >&2
exit 1
fi
}
handle_list() {
local LIMIT=20
local SINCE=""
local JSON_OUTPUT=false
while [[ $# -gt 0 ]]; do
case "$1" in
--limit) LIMIT="$2"; shift 2 ;;
--since) SINCE="$2"; shift 2 ;;
--json) JSON_OUTPUT=true; shift ;;
*) shift ;;
esac
done
if [[ "$JSON_OUTPUT" == "true" ]]; then
blog_post_list --limit "$LIMIT" ${SINCE:+--since "$SINCE"} --json | jq .
else
echo "=== Recent Digests ==="
blog_post_list --limit "$LIMIT" ${SINCE:+--since "$SINCE"}
fi
}
handle_get() {
local ID="${1:-}"
if [[ -z "$ID" ]]; then
echo "Usage: ./blog.sh get <id>" >&2
exit 1
fi
local RESULT
RESULT=$(blog_post_get "$ID")
if [[ -n "$RESULT" ]]; then
echo "$RESULT" | jq .
else
echo "❌ Digest not found: $ID" >&2
exit 1
fi
}
handle_delete() {
local ID="${1:-}"
if [[ -z "$ID" ]]; then
echo "Usage: ./blog.sh delete <id>" >&2
exit 1
fi
blog_post_delete "$ID"
}
handle_search() {
local QUERY="${1:-}"
local LIMIT=20
if [[ -z "$QUERY" ]]; then
echo "Usage: ./blog.sh search <query> [--limit N]" >&2
exit 1
fi
if [[ "${2:-}" == "--limit" && -n "${3:-}" ]]; then
LIMIT="$3"
fi
echo "=== Search Results for '$QUERY' ==="
blog_post_search "$QUERY" --limit "$LIMIT" | jq -r '.[] | "\(.id) | \(.date) | \(.content | .[0:50])..."'
}
handle_status() {
local DATE="${1:-}"
if [[ -z "$DATE" ]]; then
echo "Usage: ./blog.sh status <YYYY-MM-DD>" >&2
exit 1
fi
if blog_post_status "$DATE"; then
echo "✅ Digest exists for $DATE"
else
echo "❌ No digest found for $DATE"
fi
}
handle_health() {
echo "=== API Health Check ==="
local RESULT
RESULT=$(blog_health)
if [[ -n "$RESULT" ]]; then
echo "✅ API is accessible"
echo "$RESULT" | jq . 2>/dev/null || echo "$RESULT"
else
echo "❌ API is not responding" >&2
exit 1
fi
}
main() {
local cmd="${1:-}"
shift || true
case "$cmd" in
article-add) handle_article_add "$@" ;;
article-list) handle_article_list "$@" ;;
article-delete) handle_article_delete "$@" ;;
rss) handle_rss ;;
post) handle_post "$@" ;;
list) handle_list "$@" ;;
get) handle_get "$@" ;;
delete) handle_delete "$@" ;;
search) handle_search "$@" ;;
status) handle_status "$@" ;;
health) handle_health ;;
help|--help|-h|"") usage ;;
*)
echo "Unknown command: $cmd" >&2
usage
exit 1
;;
esac
}
main "$@"