445 lines
11 KiB
Bash
Executable File
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 "$@"
|