#!/usr/bin/env bash # # blazz — push a folder, get a URL # # blazz ./dist publish a directory (or update it on re-runs) # blazz index.html publish a single file # blazz delete delete the site recorded in .blazz/state.json # # Docs: https://blazz.ing/llms.txt # # Portability: targets macOS bash 3.2 and Linux. No associative arrays, # no ${var,,}, no mapfile. Sizes via `wc -c <` (BSD/GNU stat differ). # COPYFILE_DISABLE=1 keeps AppleDouble ._ files out of bsdtar archives. set -euo pipefail VERSION="0.1.0" BASE_URL="${BLAZZ_BASE_URL:-https://blazz.ing}" LIMIT_BYTES=6291456 MODE="publish" TARGET="" FORCE_NEW=0 SLUG="" TOKEN="" JSON=0 QUIET=0 YES=0 # ---------------------------------------------------------------- output -- if [[ -t 2 && -z "${NO_COLOR:-}" ]]; then PINK=$'\033[38;5;205m'; ORANGE=$'\033[38;5;214m'; RED=$'\033[31m' BOLD=$'\033[1m'; DIM=$'\033[2m'; OFF=$'\033[0m' else PINK=""; ORANGE=""; RED=""; BOLD=""; DIM=""; OFF="" fi say() { # decoration goes to stderr; stdout stays machine-usable if [[ "$QUIET" -eq 0 && "$JSON" -eq 0 ]]; then printf '%b\n' "$1" >&2 fi } die() { # $1 exit code, rest message local code="$1"; shift if [[ "$JSON" -eq 1 ]]; then printf '{"ok":false,"error":"%s"}\n' "$(json_escape "$*")" else printf '%b\n' "${RED}✗${OFF} $*" >&2 fi exit "$code" } usage() { cat < update a specific site (use with --token) --token update token (overrides state file) --json machine-readable output (for agents and CI) --quiet, -q print only the site URL --yes, -y don't ask before deleting --base-url API base (default: https://blazz.ing) --version print version --help, -h show this help EXAMPLES blazz publish the current directory blazz ./dist publish ./dist blazz index.html publish a single file blazz . --new new URL, even if this folder was published before Sites are anonymous and private-by-link. They expire 30 days after the last push. Max upload: 6 MB compressed. API docs: https://blazz.ing/llms.txt EOF } # ------------------------------------------------------------------ json -- # The API returns flat JSON whose string values (slugs, URLs, ISO dates, # tokens) never contain quotes or backslashes, so sed extraction is safe. json_get() { # $1 key, $2 json — also unescapes \/ and \\ in values printf '%s' "$2" \ | LC_ALL=C sed -n 's/.*"'"$1"'"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \ | head -n1 \ | LC_ALL=C sed 's,\\/,/,g; s/\\\\/\\/g' } json_get_num() { # $1 key, $2 json printf '%s' "$2" | LC_ALL=C sed -n 's/.*"'"$1"'"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' | head -n1 } json_escape() { printf '%s' "$1" | LC_ALL=C sed 's/\\/\\\\/g; s/"/\\"/g' } human_bytes() { awk -v b="$1" 'BEGIN { if (b < 1024) printf "%d B", b else if (b < 1048576) printf "%.0f KB", b / 1024 else printf "%.1f MB", b / 1048576 }' } fmt_date() { # ISO-8601 → "Jul 11, 2026"; falls back to the raw string date -j -f '%Y-%m-%dT%H:%M:%SZ' "$1" '+%b %d, %Y' 2>/dev/null \ || date -d "$1" '+%b %d, %Y' 2>/dev/null \ || printf '%s' "$1" } # ------------------------------------------------------------------ args -- while [[ $# -gt 0 ]]; do case "$1" in delete) MODE="delete" ;; --new) FORCE_NEW=1 ;; --slug) [[ $# -ge 2 ]] || die 2 "--slug needs a value"; SLUG="$2"; shift ;; --token) [[ $# -ge 2 ]] || die 2 "--token needs a value"; TOKEN="$2"; shift ;; --json) JSON=1 ;; --quiet|-q) QUIET=1 ;; --yes|-y) YES=1 ;; --base-url) [[ $# -ge 2 ]] || die 2 "--base-url needs a value"; BASE_URL="$2"; shift ;; --version) printf 'blazz %s\n' "$VERSION"; exit 0 ;; --help|-h) usage; exit 0 ;; -*) die 2 "unknown option: $1 (try --help)" ;; *) TARGET="$1" ;; esac shift done TARGET="${TARGET:-.}" BASE_URL="${BASE_URL%/}" for dep in curl tar; do command -v "$dep" >/dev/null 2>&1 || die 1 "missing dependency: $dep" done if [[ -d "$TARGET" ]]; then KIND="dir" STATE_DIR="$TARGET/.blazz" TARGET_KEY="." elif [[ -f "$TARGET" ]]; then KIND="file" STATE_DIR="$(dirname "$TARGET")/.blazz" TARGET_KEY="$(basename "$TARGET")" else die 2 "no such file or directory: $TARGET" fi STATE_FILE="$STATE_DIR/state.json" if [[ -n "$SLUG" && -z "$TOKEN" && "$MODE" == "publish" ]]; then die 2 "--slug needs --token (the update token from when the site was created)" fi # Pull slug + token from the state file unless overridden. A state file for # a different target (e.g. `blazz a.html` after `blazz .`) is left alone so # one push can't silently clobber another. if [[ -z "$SLUG" && "$FORCE_NEW" -eq 0 && -f "$STATE_FILE" ]]; then state="$(cat "$STATE_FILE")" if [[ "$(json_get target "$state")" == "$TARGET_KEY" ]]; then SLUG="$(json_get slug "$state")" TOKEN="$(json_get update_token "$state")" fi fi # ------------------------------------------------------------------ http -- RESPONSE_FILE="$(mktemp -t blazz.XXXXXX)" PAYLOAD_FILE="" trap 'rm -f "$RESPONSE_FILE" ${PAYLOAD_FILE:+"$PAYLOAD_FILE"}' EXIT http() { # $1 method, $2 url, $3 token (optional); POST/PUT send $PAYLOAD_FILE local method="$1" url="$2" token="${3:-}" local args=(-sS -o "$RESPONSE_FILE" -w '%{http_code}' -X "$method" --connect-timeout 15) if [[ "$method" == "POST" || "$method" == "PUT" ]]; then args+=(--data-binary @"$PAYLOAD_FILE" -H "Content-Type: $CONTENT_TYPE") if [[ "$KIND" == "file" && "$(basename "$TARGET")" != "index.html" ]]; then args+=(-H "X-Blazzing-Filename: $(basename "$TARGET")") fi fi [[ -n "$token" ]] && args+=(-H "X-Update-Token: $token") args+=(-A "blazz-cli/$VERSION" "$url") HTTP_CODE="$(curl "${args[@]}")" || die 4 "couldn't reach ${BASE_URL#*://} — check your connection." BODY="$(cat "$RESPONSE_FILE")" } api_error() { local message message="$(json_get message "$BODY")" case "$HTTP_CODE" in 401) die 5 "that update token doesn't match ${SLUG:-this site}. Pass the right one with --token, or run blazz --new for a fresh site." ;; 413) die 5 "${message:-the upload is too large (6 MB max, compressed).}" ;; 429) die 5 "rate limited — wait a minute and try again." ;; *) die 5 "${message:-the API returned HTTP $HTTP_CODE.}" ;; esac } # ---------------------------------------------------------------- delete -- if [[ "$MODE" == "delete" ]]; then [[ -n "$SLUG" ]] || die 2 "no site recorded for $TARGET — nothing to delete (or pass --slug and --token)" [[ -n "$TOKEN" ]] || die 2 "no update token for $SLUG — pass it with --token" if [[ "$YES" -eq 0 && "$JSON" -eq 0 ]]; then printf 'delete %s? [y/N] ' "$SLUG" >&2 read -r answer case "$answer" in y|Y|yes|YES) ;; *) die 1 "aborted." ;; esac fi http DELETE "$BASE_URL/api/sites/$SLUG" "$TOKEN" [[ "$HTTP_CODE" == "204" || "$HTTP_CODE" == "404" ]] || api_error rm -f "$STATE_FILE" if [[ "$JSON" -eq 1 ]]; then printf '{"ok":true,"action":"deleted","slug":"%s"}\n' "$SLUG" else say "${ORANGE}⚡${OFF} deleted ${BOLD}$SLUG${OFF} — it has blazzed away." fi exit 0 fi # --------------------------------------------------------------- payload -- if [[ "$KIND" == "dir" ]]; then PAYLOAD_FILE="$(mktemp -t blazz.XXXXXX)" COPYFILE_DISABLE=1 tar -czf "$PAYLOAD_FILE" \ --exclude '.git' \ --exclude 'node_modules' \ --exclude '.DS_Store' \ --exclude '.blazz' \ -C "$TARGET" . CONTENT_TYPE="application/gzip" else PAYLOAD_FILE="$(mktemp -t blazz.XXXXXX)" cat "$TARGET" > "$PAYLOAD_FILE" case "$TARGET" in *.html|*.htm) CONTENT_TYPE="text/html" ;; *.zip) CONTENT_TYPE="application/zip" ;; *.tar.gz|*.tgz) CONTENT_TYPE="application/gzip" ;; *.pdf) CONTENT_TYPE="application/pdf" ;; *.txt|*.md) CONTENT_TYPE="text/plain" ;; *) CONTENT_TYPE="application/octet-stream" ;; esac fi BYTES="$(wc -c < "$PAYLOAD_FILE" | tr -d '[:space:]')" if [[ "$BYTES" -gt "$LIMIT_BYTES" ]]; then say "${DIM}blazz already skips .git and node_modules; look for videos, raw images, or build artifacts.${OFF}" die 3 "too big: $TARGET is $(human_bytes "$BYTES") compressed — the limit is 6 MB." fi # --------------------------------------------------------------- publish -- ACTION="created" if [[ -n "$SLUG" && -n "$TOKEN" ]]; then say "${ORANGE}⚡${OFF} ${BOLD}blazz${OFF}" say "" say " packed $TARGET — $(human_bytes "$BYTES") compressed" say " updating ${BOLD}$SLUG${OFF} ..." http PUT "$BASE_URL/api/sites/$SLUG" "$TOKEN" if [[ "$HTTP_CODE" == "404" ]]; then say " previous site expired — publishing a fresh one" http POST "$BASE_URL/api/sites" ACTION="recreated" else ACTION="updated" fi else say "${ORANGE}⚡${OFF} ${BOLD}blazz${OFF}" say "" say " packed $TARGET — $(human_bytes "$BYTES") compressed" say " pushing to ${BASE_URL#*://} ..." fi if [[ "$ACTION" == "created" ]]; then http POST "$BASE_URL/api/sites" fi case "$HTTP_CODE" in 2*) ;; *) api_error ;; esac NEW_SLUG="$(json_get slug "$BODY")" URL="$(json_get url "$BODY")" EXPIRES="$(json_get expires_at "$BODY")" FILES="$(json_get_num files "$BODY")" NEW_TOKEN="$(json_get update_token "$BODY")" [[ -n "$NEW_TOKEN" ]] || NEW_TOKEN="$TOKEN" [[ -n "$URL" ]] || die 1 "unexpected response from the API: $BODY" # ----------------------------------------------------------------- state -- mkdir -p "$STATE_DIR" printf '{\n "slug": "%s",\n "url": "%s",\n "update_token": "%s",\n "expires_at": "%s",\n "target": "%s"\n}\n' \ "$NEW_SLUG" "$URL" "$NEW_TOKEN" "$EXPIRES" "$(json_escape "$TARGET_KEY")" > "$STATE_FILE" printf '*\n' > "$STATE_DIR/.gitignore" # ---------------------------------------------------------------- output -- if [[ "$JSON" -eq 1 ]]; then printf '{"ok":true,"action":"%s","slug":"%s","url":"%s","expires_at":"%s","update_token":"%s","files":%s,"bytes":%s}\n' \ "$ACTION" "$NEW_SLUG" "$URL" "$EXPIRES" "$NEW_TOKEN" "${FILES:-1}" "$BYTES" elif [[ "$QUIET" -eq 1 ]]; then printf '%s\n' "$URL" else say "" if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then printf '%b\n' " ${BOLD}${PINK}$URL${OFF}" else printf '%s\n' "$URL" fi say "" say " ${DIM}expires${OFF} $(fmt_date "$EXPIRES") — push again to reset the 30-day clock" say " ${DIM}update${OFF} just run \`blazz $TARGET\` again from here" say " ${DIM}token${OFF} saved to $STATE_FILE ${DIM}(keep it; it can't be re-issued)${OFF}" fi