← Commit history

Delete headview

barrett-land ·9f8f1b57ed · ·parent 097aae4
1 file changed −306
headviewdeleted−306
@@ -1,306 +0,0 @@-#!/usr/bin/env bash-# headview — turn a headless Adom container into a browser-accessible desktop.-#-# Spins up Xvfb + fluxbox + x11vnc + noVNC inside the container, maps a-# *.adom.cloud port so Cloudflare serves it, and prints a URL you can open-# from any browser (or as a Hydrogen webview tab). The container hosts the-# site itself — no SSH tunnel, no local viewer.-#-# Design: AI-oriented output — every line starts with OK: or ERROR:, errors-# carry a Hint:. Colors only on a TTY (stripped when piped/logged).--set -uo pipefail--HEADVIEW_VERSION="0.1.0"--# ---------------------------------------------------------------- output helpers-if [ -t 1 ]; then C_OK=$'\033[32m\033[1m'; C_CYAN=$'\033[36m'; C_YEL=$'\033[33m'; C_DIM=$'\033[2m'; C_RST=$'\033[0m'-else C_OK=""; C_CYAN=""; C_YEL=""; C_DIM=""; C_RST=""; fi-if [ -t 2 ]; then E_ERR=$'\033[31m\033[1m'; E_DIM=$'\033[2m'; E_RST=$'\033[0m'; else E_ERR=""; E_DIM=""; E_RST=""; fi--ok()   { printf '%sOK:%s %s\n'   "$C_OK"  "$C_RST" "$*"; }-note() { printf '%s%s%s\n'       "$C_DIM" "$*" "$C_RST"; }-err()  { printf '%sERROR:%s %s\n' "$E_ERR" "$E_RST" "$*" >&2; }-hint() { printf '%sHint:%s %s\n' "$E_DIM" "$E_RST" "$*" >&2; }-die()  { err "$1"; [ -n "${2:-}" ] && hint "$2"; exit 1; }--# ---------------------------------------------------------------- state-STATE_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/headview"-STATE_FILE="$STATE_DIR/state.env"-NOVNC_HOME="$HOME/.local/share/novnc"     # git-clone fallback web root-mkdir -p "$STATE_DIR"--save_state() { # key value-  mkdir -p "$STATE_DIR"; touch "$STATE_FILE"-  grep -v "^$1=" "$STATE_FILE" 2>/dev/null > "$STATE_FILE.tmp" || true-  echo "$1=$2" >> "$STATE_FILE.tmp"; mv "$STATE_FILE.tmp" "$STATE_FILE"-}-load_state() { [ -f "$STATE_FILE" ] && set -a && . "$STATE_FILE" && set +a || true; }--# ---------------------------------------------------------------- defaults-DISPLAY_NUM=":99"-GEOMETRY="1600x1000"-DEPTH="24"-VNC_PORT="5900"-WEB_PORT="6080"-PREFIX="desktop"-APP=""-USE_PASSWORD="1"--# ---------------------------------------------------------------- deps-have() { command -v "$1" >/dev/null 2>&1; }--novnc_webroot() {-  if [ -f /usr/share/novnc/vnc.html ]; then echo /usr/share/novnc-  elif [ -f "$NOVNC_HOME/vnc.html" ]; then echo "$NOVNC_HOME"-  else echo ""; fi-}--ensure_deps() {-  local need=()-  have Xvfb    || need+=(xvfb)-  have x11vnc  || need+=(x11vnc)-  have fluxbox || need+=(fluxbox)-  have xdpyinfo|| need+=(x11-utils)-  # noVNC + websockify: prefer apt packages-  if [ -z "$(novnc_webroot)" ] || ! have websockify; then need+=(novnc websockify); fi-  if [ ${#need[@]} -gt 0 ]; then-    note "installing: ${need[*]}"-    if have sudo; then sudo apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${need[@]}" >/dev/null 2>&1-    else apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${need[@]}" >/dev/null 2>&1; fi-  fi-  # git-clone fallback if apt didn't provide a web root-  if [ -z "$(novnc_webroot)" ]; then-    note "apt novnc unavailable — cloning noVNC from GitHub"-    have git || die "git not available and noVNC not installed" "sudo apt-get install -y git novnc"-    [ -d "$NOVNC_HOME" ] || git clone --depth 1 -q https://github.com/novnc/noVNC "$NOVNC_HOME" || \-      die "failed to fetch noVNC" "check the container has outbound network to github.com"-    [ -d "$NOVNC_HOME/utils/websockify" ] || git clone --depth 1 -q https://github.com/novnc/websockify "$NOVNC_HOME/utils/websockify" || true-  fi-  for b in Xvfb x11vnc fluxbox; do have "$b" || die "$b still missing after install" "install it manually: sudo apt-get install -y $b"; done-}--# ---------------------------------------------------------------- self slug-container_slug() {-  local j; j="$(adom-cli carbon containers current 2>/dev/null)" || return 1-  printf '%s' "$j" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("slug",""))' 2>/dev/null-}--# find an existing mapped hostname whose internal port == WEB_PORT-existing_hostname_for_port() {-  local port="$1" j-  j="$(adom-cli carbon containers current 2>/dev/null)" || return 1-  printf '%s' "$j" | python3 -c '-import sys,json-p=int(sys.argv[1]); d=json.load(sys.stdin)-for host,port in (d.get("port_mappings") or {}).items():-    if int(port)==p: print(host); break-' "$port" 2>/dev/null-}--start_websockify() {-  local webroot="$1"-  pkill -f "websockify.*:$WEB_PORT" 2>/dev/null; pkill -f "novnc_proxy.*$WEB_PORT" 2>/dev/null; sleep 1-  if have websockify; then-    nohup websockify --web="$webroot" "0.0.0.0:$WEB_PORT" "localhost:$VNC_PORT" >/tmp/headview-web.log 2>&1 &-  else-    nohup "$NOVNC_HOME/utils/novnc_proxy" --vnc "localhost:$VNC_PORT" --listen "$WEB_PORT" >/tmp/headview-web.log 2>&1 &-  fi-}--# ---------------------------------------------------------------- commands-cmd_up() {-  ensure_deps-  local webroot; webroot="$(novnc_webroot)"-  [ -n "$webroot" ] || die "noVNC web root not found" "run: sudo apt-get install -y novnc"--  export DISPLAY="$DISPLAY_NUM"--  # 1) Xvfb-  if ! pgrep -f "Xvfb $DISPLAY_NUM" >/dev/null; then-    nohup Xvfb "$DISPLAY_NUM" -screen 0 "${GEOMETRY}x${DEPTH}" >/tmp/headview-xvfb.log 2>&1 &-    sleep 2-  fi-  pgrep -f "Xvfb $DISPLAY_NUM" >/dev/null || die "Xvfb failed to start" "check /tmp/headview-xvfb.log"--  # 2) window manager-  pgrep -f "fluxbox" >/dev/null || { nohup fluxbox >/tmp/headview-fluxbox.log 2>&1 &  sleep 1; }--  # 3) password-  local pw=""-  if [ "$USE_PASSWORD" = "1" ]; then-    pw="${HEADVIEW_PASSWORD:-}"-    if [ -z "$pw" ]; then pw="hv$(od -An -N4 -tx1 /dev/urandom | tr -d ' \n')"; fi-    mkdir -p "$HOME/.vnc"-    x11vnc -storepasswd "$pw" "$HOME/.vnc/passwd" >/dev/null 2>&1-    save_state PASSWORD "$pw"-  else-    save_state PASSWORD ""-  fi--  # 4) x11vnc (loopback only — noVNC is the public face)-  pkill -f "x11vnc.*:$VNC_PORT" 2>/dev/null; pkill -f "x11vnc -display $DISPLAY_NUM" 2>/dev/null; sleep 1-  if [ "$USE_PASSWORD" = "1" ]; then-    nohup x11vnc -display "$DISPLAY_NUM" -localhost -rfbauth "$HOME/.vnc/passwd" -forever -shared -rfbport "$VNC_PORT" -bg >/tmp/headview-x11vnc.log 2>&1-  else-    nohup x11vnc -display "$DISPLAY_NUM" -localhost -nopw -forever -shared -rfbport "$VNC_PORT" -bg >/tmp/headview-x11vnc.log 2>&1-  fi-  sleep 2-  grep -q "Listening for VNC" /tmp/headview-x11vnc.log || die "x11vnc failed to listen on $VNC_PORT" "check /tmp/headview-x11vnc.log"--  # 5) noVNC web bridge-  start_websockify "$webroot"; sleep 3-  curl -s -o /dev/null --max-time 6 "http://127.0.0.1:$WEB_PORT/vnc.html" || die "noVNC not serving on $WEB_PORT" "check /tmp/headview-web.log"--  # 6) port mapping-  local host; host="$(existing_hostname_for_port "$WEB_PORT")"-  if [ -z "$host" ]; then-    local slug; slug="$(container_slug)"-    [ -n "$slug" ] || die "could not determine this container's slug" "run inside an Adom container; check: adom-cli carbon containers current"-    note "adding port mapping ${WEB_PORT} -> ${PREFIX}-*.adom.cloud"-    local out; out="$(adom-cli carbon containers port-add --port "$WEB_PORT" --prefix "$PREFIX" "$slug" 2>&1)" || die "port-add failed: $out" "check adom-cli auth"-    host="$(printf '%s' "$out" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("hostname",""))' 2>/dev/null)"-  fi-  [ -n "$host" ] || die "no mapped hostname" "run: adom-cli carbon containers current  # to inspect mappings"--  # 7) optional app-  if [ -n "$APP" ]; then DISPLAY="$DISPLAY_NUM" nohup $APP >/tmp/headview-app.log 2>&1 & note "launched app: $APP"; fi--  save_state DISPLAY_NUM "$DISPLAY_NUM"-  save_state GEOMETRY "$GEOMETRY"-  save_state VNC_PORT "$VNC_PORT"-  save_state WEB_PORT "$WEB_PORT"-  save_state HV_HOST "$host"--  local url="https://$host/vnc.html?autoconnect=1&resize=scale"-  ok "Container desktop is live and hosted by the container itself."-  note "  URL:      $url"-  [ "$USE_PASSWORD" = "1" ] && note "  Password: $pw"-  note "  Display:  $DISPLAY_NUM (${GEOMETRY})   VNC:$VNC_PORT  web:$WEB_PORT"-  note "Open it in any browser, or as a Hydrogen tab:  headview open"-  note "Launch apps onto it with:  DISPLAY=$DISPLAY_NUM your-app &"-}--cmd_down() {-  load_state-  local removed_map=""-  pkill -f "websockify.*:${WEB_PORT:-6080}" 2>/dev/null-  pkill -f "novnc_proxy" 2>/dev/null-  pkill -f "x11vnc.*:${VNC_PORT:-5900}" 2>/dev/null-  pkill -f "x11vnc -display ${DISPLAY_NUM:-:99}" 2>/dev/null-  pkill -f "fluxbox" 2>/dev/null-  pkill -f "Xvfb ${DISPLAY_NUM:-:99}" 2>/dev/null-  if [ "${1:-}" = "--keep-mapping" ]; then :; else-    if [ -n "${HV_HOST:-}" ]; then-      local slug sub; slug="$(container_slug)"; sub="${HV_HOST%.adom.cloud}"-      if [ -n "$slug" ] && [ -n "$sub" ]; then-        adom-cli carbon containers port-remove "$slug" "$sub" >/dev/null 2>&1 && removed_map=" and removed port mapping ($sub)" || true-      fi-    fi-  fi-  ok "Tore down headview desktop$removed_map."-}--cmd_status() {-  load_state-  local up=0-  printf 'Xvfb:       ' ; pgrep -f "Xvfb ${DISPLAY_NUM:-:99}" >/dev/null && { echo running; up=1; } || echo stopped-  printf 'x11vnc:     ' ; pgrep -f "x11vnc" >/dev/null && echo running || echo stopped-  printf 'websockify: ' ; pgrep -f "websockify|novnc_proxy" >/dev/null && echo running || echo stopped-  if [ -n "${HV_HOST:-}" ]; then note "URL:      https://$HV_HOST/vnc.html?autoconnect=1&resize=scale"; [ -n "${PASSWORD:-}" ] && note "Password: $PASSWORD"; fi-  [ "$up" = 1 ] && ok "headview is up." || ok "headview is not running."-}--cmd_url() {-  load_state-  [ -n "${HV_HOST:-}" ] || die "no desktop is running" "start one with: headview up"-  echo "https://$HV_HOST/vnc.html?autoconnect=1&resize=scale"-}--cmd_password() {-  load_state-  [ -n "${PASSWORD:-}" ] && ok "VNC password: $PASSWORD" || ok "no password set (passwordless mode)"-}--cmd_open() {-  load_state-  [ -n "${HV_HOST:-}" ] || die "no desktop is running" "start one with: headview up"-  [ -n "${VSCODE_PROXY_URI:-}" ] || die "Hydrogen editor not detected (VSCODE_PROXY_URI unset)" "run this from a Hydrogen workspace terminal, or open the URL in a browser: headview url"-  local url="https://$HV_HOST/vnc.html?autoconnect=1&resize=scale"-  adom-cli hydrogen webview open-or-refresh --name "Container Desktop" --url "$url" && ok "Opened 'Container Desktop' webview tab." || die "failed to open webview" "check adom-cli hydrogen webview"-}--cmd_install() {-  local skill_dir="$HOME/.claude/skills/headview"-  mkdir -p "$skill_dir"-  local url="https://wiki.adom.inc/static/apps/headview/SKILL.md"-  if curl -fsSL --max-time 5 "$url" -o "$skill_dir/SKILL.md" 2>/dev/null; then-    ok "Installed headview skill (fetched from wiki)."-  elif [ -f "$(dirname "$0")/SKILL.md" ]; then-    cp "$(dirname "$0")/SKILL.md" "$skill_dir/SKILL.md"; ok "Installed headview skill (bundled copy; wiki unreachable)."-  else-    die "could not install skill" "SKILL.md not found next to the binary and wiki unreachable"-  fi-}--usage() {-  cat <<EOF-headview v$HEADVIEW_VERSION — a browser desktop for a headless Adom container--USAGE: headview <command> [options]--COMMANDS:-  up            Start the desktop (Xvfb+fluxbox+x11vnc+noVNC), map a port, print the URL-  down          Stop everything and remove the port mapping (--keep-mapping to keep it)-  status        Show what's running and the URL/password-  url           Print the desktop URL-  password      Print the VNC password-  open          Open the desktop as a Hydrogen webview tab (needs a Hydrogen terminal)-  install       Install the headview skill into ~/.claude/skills/-  version       Print version--'up' OPTIONS:-  --display :N        X display (default :99)-  --geometry WxH      screen size (default 1600x1000)-  --vnc-port N        internal VNC port (default 5900)-  --web-port N        internal noVNC/web port to map (default 6080)-  --prefix NAME       subdomain prefix for the mapping (default desktop)-  --app "CMD"         launch CMD on the desktop after starting-  --no-password       run passwordless (only do this behind other access control)--EXAMPLES:-  headview up-  headview up --geometry 1920x1080 --app "kicad"-  headview url-  headview down-EOF-}--# ---------------------------------------------------------------- arg parse-cmd="${1:-}"; shift || true-while [ $# -gt 0 ]; do-  case "$1" in-    --display)  DISPLAY_NUM="$2"; shift 2;;-    --geometry) GEOMETRY="$2"; shift 2;;-    --vnc-port) VNC_PORT="$2"; shift 2;;-    --web-port) WEB_PORT="$2"; shift 2;;-    --prefix)   PREFIX="$2"; shift 2;;-    --app)      APP="$2"; shift 2;;-    --no-password) USE_PASSWORD="0"; shift;;-    --keep-mapping) KEEP_MAPPING="--keep-mapping"; shift;;-    *) shift;;-  esac-done--case "$cmd" in-  up)       cmd_up;;-  down)     cmd_down "${KEEP_MAPPING:-}";;-  status)   cmd_status;;-  url)      cmd_url;;-  password) cmd_password;;-  open)     cmd_open;;-  install)  cmd_install;;-  version|--version|-V) ok "headview v$HEADVIEW_VERSION";;-  ""|help|-h|--help) usage;;-  *) err "unknown command: $cmd"; hint "run 'headview help' for usage"; exit 1;;-esac