← Commit history

Rename headview -> The Headless Deskman (headless-deskman); 0.2.0

barrett-land ·097aae4c0e · ·parent 291cc4d
7 files changed +428−93
README.md+35−28
@@ -1,50 +1,56 @@-# headview+# The Headless Deskman  **A browser desktop for a headless Adom container.** -`headview` gives a headless container a real GUI you can open in any browser. It-starts a virtual X display in the container, scrapes it with VNC, bridges that to-WebSocket with noVNC, and maps a `*.adom.cloud` port so **Cloudflare serves the-page — the container hosts it itself.** No SSH tunnel, no local VNC viewer, no X-server on your machine. Open the URL on a laptop, a phone, or as a Hydrogen-webview tab.+`headless-deskman` gives a headless container a real GUI you can open in any+browser. It starts a virtual X display in the container, scrapes it with VNC,+bridges that to WebSocket with noVNC, and maps a `*.adom.cloud` port so+**Cloudflare serves the page — the container hosts it itself.** No SSH tunnel, no+local VNC viewer, no X server on your machine. Open the URL on a laptop, a phone,+or as a Hydrogen webview tab. -![KiCad running in a headless container, viewed in a browser via headview](docs/screenshot-desktop.png)+![KiCad running in a headless container, viewed in a browser via The Headless Deskman](docs/screenshot-desktop.png)++> **Renamed in 0.2.0.** This app was `headview`. The command is now+> `headless-deskman` — installing 0.2.0 removes the old `headview` binary, and a+> desktop you started under the old name keeps working (its state migrates from+> `~/.config/headview` automatically).  ## Why not `ssh -Y`?  X11 forwarding does **not** work through the `adom.cloud` SSH gateway: the gateway relays your connection into the container, so the X11 channel never reaches the container's `sshd` (`DISPLAY` comes back empty even with `X11Forwarding yes` and-`xauth` installed). `headview` sidesteps that entirely by serving over+`xauth` installed). The Headless Deskman sidesteps that entirely by serving over HTTP/WebSocket, which the gateway and Cloudflare pass fine.  ## Install  ```bash-adom-wiki pkg install barrett-land/headview+adom-wiki pkg install barrett-land/headless-deskman ```  ## Quick start  ```bash # inside the target container:-headview up                     # start the desktop, map a port, print the URL-headview up --app "kicad"       # ...with KiCad already open on it-headview url                    # print the URL again-headview status                 # what's running + URL + password-headview down                   # stop everything and remove the port mapping+headless-deskman up                     # start the desktop, map a port, print the URL+headless-deskman up --app "kicad"       # ...with KiCad already open on it+headless-deskman url                    # print the URL again+headless-deskman status                 # what's running + URL + password+headless-deskman down                   # stop everything and remove the port mapping ``` -`headview up` prints a URL like `https://desktop-<id>.adom.cloud/vnc.html` and a-generated VNC password. Open it anywhere. To launch more apps onto the desktop:+`headless-deskman up` prints a URL like `https://desktop-<id>.adom.cloud/vnc.html`+and a generated VNC password. Open it anywhere. To launch more apps onto the+desktop:  ```bash DISPLAY=:99 pcbnew & DISPLAY=:99 xterm & ``` -Open it as a Hydrogen tab (from a Hydrogen workspace terminal): `headview open`.+Open it as a Hydrogen tab (from a Hydrogen workspace terminal): `headless-deskman open`.  ## The stack @@ -57,18 +63,19 @@ Xvfb (:99)  →  fluxbox  →  x11vnc (localhost:5900)  →  noVNC/websockify (: - **Software rendering only.** Xvfb has no GPU, so 2D GUIs are great but   OpenGL/3D views (KiCad 3D viewer, Blender viewport) won't render well. - **Security.** The VNC server binds `localhost`; the public face is the noVNC-  HTTPS URL, protected by the generated VNC password (`headview password`).-- Run `headview` **inside** the target container — it auto-detects the container-  slug via `adom-cli` and maps its own port.+  HTTPS URL, protected by the generated VNC password (`headless-deskman password`).+  Set `DESKMAN_PASSWORD` to pin it instead of generating one.+- Run `headless-deskman` **inside** the target container — it auto-detects the+  container slug via `adom-cli` and maps its own port.  ## Commands  | Command | What it does | |---|---|-| `headview up` | start desktop, map port, print URL (`--display`, `--geometry`, `--vnc-port`, `--web-port`, `--prefix`, `--app`, `--no-password`) |-| `headview down` | stop everything, remove the mapping (`--keep-mapping` to keep it) |-| `headview status` | show running processes, URL, password |-| `headview url` | print the desktop URL |-| `headview password` | print the VNC password |-| `headview open` | open as a Hydrogen webview tab (needs a Hydrogen terminal) |-| `headview install` | (re)install the skill from the wiki |+| `headless-deskman up` | start desktop, map port, print URL (`--display`, `--geometry`, `--vnc-port`, `--web-port`, `--prefix`, `--app`, `--no-password`) |+| `headless-deskman down` | stop everything, remove the mapping (`--keep-mapping` to keep it) |+| `headless-deskman status` | show running processes, URL, password |+| `headless-deskman url` | print the desktop URL |+| `headless-deskman password` | print the VNC password |+| `headless-deskman open` | open as a Hydrogen webview tab (needs a Hydrogen terminal) |+| `headless-deskman install` | (re)install the skill from the wiki |
SKILL.md+26−20
@@ -1,20 +1,24 @@ ----name: headview-description: "Turn a headless Adom container into a browser-accessible desktop. Runs Xvfb+fluxbox+x11vnc+noVNC inside the container and maps a *.adom.cloud port so any browser (or a Hydrogen webview tab) shows the container's GUI. Trigger words: headless desktop, container desktop, view GUI from container, noVNC, run a GUI app in the container, remote desktop for container, browser desktop, X server in container, see kicad/gui in browser."+name: headless-deskman+description: "The Headless Deskman — turn a headless Adom container into a browser-accessible desktop. Runs Xvfb+fluxbox+x11vnc+noVNC inside the container and maps a *.adom.cloud port so any browser (or a Hydrogen webview tab) shows the container's GUI. Trigger words: headless desktop, container desktop, headless deskman, deskman, headview (old name), view GUI from container, noVNC, run a GUI app in the container, remote desktop for container, browser desktop, X server in container, see kicad/gui in browser." --- -# headview+# The Headless Deskman -`headview` gives a **headless** Adom container a real desktop you can open in a-browser. It starts a virtual X display in the container, scrapes it with VNC,+`headless-deskman` gives a **headless** Adom container a real desktop you can open+in a browser. It starts a virtual X display in the container, scrapes it with VNC, bridges that to WebSocket with noVNC, and maps a `*.adom.cloud` port so **Cloudflare serves the page — the container hosts it itself**, no SSH tunnel and no local VNC viewer. Open the URL anywhere, or drop it into a Hydrogen webview tab. +> Renamed in 0.2.0: this was `headview`. The command is now `headless-deskman`; the+> old binary is removed on install, and a desktop started under the old name is+> picked up automatically (state migrates from `~/.config/headview`).+ Why not `ssh -Y`? X11 forwarding does **not** work through the `adom.cloud` SSH gateway (the gateway relays the connection, so the X11 channel never reaches the-container). `headview` sidesteps that entirely by serving over HTTP/WebSocket,-which the gateway/Cloudflare pass fine.+container). The Headless Deskman sidesteps that entirely by serving over+HTTP/WebSocket, which the gateway/Cloudflare pass fine.  ## Stack @@ -25,7 +29,7 @@ OpenGL/3D views.  ## Commands -### `headview up`+### `headless-deskman up` Install deps if missing, start the desktop, map the port, print the URL + password. Idempotent — safe to re-run.  | Flag | Default | Description |@@ -39,24 +43,26 @@ Install deps if missing, start the desktop, map the port, print the URL + passwo | `--no-password` | — | passwordless (only behind other access control) |  ```bash-headview up                              # bare desktop-headview up --app "kicad"                # desktop with KiCad already open-headview up --geometry 1920x1080+headless-deskman up                              # bare desktop+headless-deskman up --app "kicad"                # desktop with KiCad already open+headless-deskman up --geometry 1920x1080 ``` -### `headview open`+Set `DESKMAN_PASSWORD` to pin the VNC password instead of generating one.++### `headless-deskman open` Open the running desktop as a Hydrogen webview tab named "Container Desktop". Must be run from a **Hydrogen workspace terminal** (needs `VSCODE_PROXY_URI`).-From anywhere else, use `headview url` and open it in a browser.+From anywhere else, use `headless-deskman url` and open it in a browser. -### `headview status` / `headview url` / `headview password`+### `headless-deskman status` / `url` / `password` Inspect state: what's running, the URL, and the VNC password. -### `headview down`+### `headless-deskman down` Stop Xvfb/fluxbox/x11vnc/noVNC and **remove the port mapping**. Add `--keep-mapping` to leave the mapping in place. -### `headview install`-Install/refresh this skill from the wiki into `~/.claude/skills/headview/`.+### `headless-deskman install`+Install/refresh this skill from the wiki into `~/.claude/skills/headless-deskman/`.  ## Launching apps onto the desktop Anything you run with `DISPLAY=:99` appears in the browser window:@@ -66,9 +72,9 @@ DISPLAY=:99 xterm & ```  ## Notes-- Run `headview` **inside the target container** — it auto-detects the container-  slug via `adom-cli carbon containers current` and maps its own port.+- Run `headless-deskman` **inside the target container** — it auto-detects the+  container slug via `adom-cli carbon containers current` and maps its own port. - The VNC server binds `localhost` only; the public face is the noVNC HTTPS URL,-  protected by the generated VNC password (shown by `headview up`/`password`).+  protected by the generated VNC password (shown by `up`/`password`). - WebSocket idle: Cloudflare can drop very idle WS connections; noVNC reconnects,   and any desktop activity keeps it warm.
headless-deskmanadded+319
@@ -0,0 +1,319 @@+#!/usr/bin/env bash+# headless-deskman — The Headless Deskman: 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++DESKMAN_VERSION="0.2.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}/headless-deskman"+STATE_FILE="$STATE_DIR/state.env"+LEGACY_STATE_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/headview"   # pre-0.2.0 name+NOVNC_HOME="$HOME/.local/share/novnc"     # git-clone fallback web root+mkdir -p "$STATE_DIR"++# Adopt state left by the old `headview` name so a desktop started before the+# rename stays reachable (URL + password) instead of looking "not running".+if [ ! -f "$STATE_FILE" ] && [ -f "$LEGACY_STATE_DIR/state.env" ]; then+  cp "$LEGACY_STATE_DIR/state.env" "$STATE_FILE" 2>/dev/null || true+fi++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+  # HV_HOST was the pre-0.2.0 key for the mapped hostname.+  [ -z "${HD_HOST:-}" ] && [ -n "${HV_HOST:-}" ] && HD_HOST="$HV_HOST"+  return 0+}++# ---------------------------------------------------------------- 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/headless-deskman-web.log 2>&1 &+  else+    nohup "$NOVNC_HOME/utils/novnc_proxy" --vnc "localhost:$VNC_PORT" --listen "$WEB_PORT" >/tmp/headless-deskman-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/headless-deskman-xvfb.log 2>&1 &+    sleep 2+  fi+  pgrep -f "Xvfb $DISPLAY_NUM" >/dev/null || die "Xvfb failed to start" "check /tmp/headless-deskman-xvfb.log"++  # 2) window manager+  pgrep -f "fluxbox" >/dev/null || { nohup fluxbox >/tmp/headless-deskman-fluxbox.log 2>&1 &  sleep 1; }++  # 3) password+  local pw=""+  if [ "$USE_PASSWORD" = "1" ]; then+    pw="${DESKMAN_PASSWORD:-${HEADVIEW_PASSWORD:-}}"   # HEADVIEW_PASSWORD: pre-0.2.0 name+    if [ -z "$pw" ]; then pw="hd$(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/headless-deskman-x11vnc.log 2>&1+  else+    nohup x11vnc -display "$DISPLAY_NUM" -localhost -nopw -forever -shared -rfbport "$VNC_PORT" -bg >/tmp/headless-deskman-x11vnc.log 2>&1+  fi+  sleep 2+  grep -q "Listening for VNC" /tmp/headless-deskman-x11vnc.log || die "x11vnc failed to listen on $VNC_PORT" "check /tmp/headless-deskman-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/headless-deskman-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/headless-deskman-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 HD_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:  headless-deskman 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 "${HD_HOST:-}" ]; then+      local slug sub; slug="$(container_slug)"; sub="${HD_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 the Headless Deskman 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 "${HD_HOST:-}" ]; then note "URL:      https://$HD_HOST/vnc.html?autoconnect=1&resize=scale"; [ -n "${PASSWORD:-}" ] && note "Password: $PASSWORD"; fi+  [ "$up" = 1 ] && ok "The Headless Deskman is up." || ok "The Headless Deskman is not running."+}++cmd_url() {+  load_state+  [ -n "${HD_HOST:-}" ] || die "no desktop is running" "start one with: headless-deskman up"+  echo "https://$HD_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 "${HD_HOST:-}" ] || die "no desktop is running" "start one with: headless-deskman 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: headless-deskman url"+  local url="https://$HD_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/headless-deskman"+  mkdir -p "$skill_dir"+  local url="https://wiki.adom.inc/static/apps/headless-deskman/SKILL.md"+  if curl -fsSL --max-time 5 "$url" -o "$skill_dir/SKILL.md" 2>/dev/null; then+    ok "Installed headless-deskman skill (fetched from wiki)."+  elif [ -f "$(dirname "$0")/SKILL.md" ]; then+    cp "$(dirname "$0")/SKILL.md" "$skill_dir/SKILL.md"; ok "Installed headless-deskman 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+The Headless Deskman v$DESKMAN_VERSION — a browser desktop for a headless Adom container++USAGE: headless-deskman <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 headless-deskman 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:+  headless-deskman up+  headless-deskman up --geometry 1920x1080 --app "kicad"+  headless-deskman url+  headless-deskman 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 "The Headless Deskman v$DESKMAN_VERSION (headless-deskman)";;+  ""|help|-h|--help) usage;;+  *) err "unknown command: $cmd"; hint "run 'headless-deskman help' for usage"; exit 1;;+esac
install.sh+18−15
@@ -1,17 +1,17 @@ #!/bin/sh-# headview install — put the CLI on PATH. Skills are placed by the wiki installer.+# The Headless Deskman install — put the CLI on PATH. Skills are placed by the wiki installer. set -e SRC="$(cd "$(dirname "$0")" && pwd)"  install_to() {-  cp "$SRC/headview" "$1/headview"-  chmod +x "$1/headview"-  echo "installed headview -> $1/headview"+  cp "$SRC/headless-deskman" "$1/headless-deskman"+  chmod +x "$1/headless-deskman"+  echo "installed headless-deskman -> $1/headless-deskman" } -if command -v sudo >/dev/null 2>&1 && sudo -n cp "$SRC/headview" /usr/local/bin/headview 2>/dev/null; then-  sudo chmod +x /usr/local/bin/headview-  echo "installed headview -> /usr/local/bin/headview"+if command -v sudo >/dev/null 2>&1 && sudo -n cp "$SRC/headless-deskman" /usr/local/bin/headless-deskman 2>/dev/null; then+  sudo chmod +x /usr/local/bin/headless-deskman+  echo "installed headless-deskman -> /usr/local/bin/headless-deskman" elif [ -w /usr/local/bin ]; then   install_to /usr/local/bin else@@ -19,15 +19,18 @@ else   install_to "$HOME/.local/bin"   case ":$PATH:" in     *":$HOME/.local/bin:"*) : ;;-    *) echo "note: add \$HOME/.local/bin to your PATH to run headview" ;;+    *) echo "note: add \$HOME/.local/bin to your PATH to run headless-deskman" ;;   esac fi -# place the user skill where Claude Code discovers it-if [ -f "$SRC/SKILL.md" ]; then-  mkdir -p "$HOME/.claude/skills/headview"-  cp "$SRC/SKILL.md" "$HOME/.claude/skills/headview/SKILL.md"-  echo "installed skill -> $HOME/.claude/skills/headview/SKILL.md"-fi+# Retire the pre-0.2.0 `headview` binary so the two names can't drift apart.+for d in /usr/local/bin "$HOME/.local/bin"; do+  if [ -e "$d/headview" ]; then+    if [ -w "$d" ]; then rm -f "$d/headview"+    elif command -v sudo >/dev/null 2>&1; then sudo rm -f "$d/headview" 2>/dev/null || true+    fi+    echo "removed old $d/headview (renamed to headless-deskman)"+  fi+done -echo "run: headview up"+echo "run: headless-deskman up"
package.json+10−15
@@ -1,18 +1,10 @@ {-  "slug": "headview",-  "title": "Headview",+  "slug": "headless-deskman",+  "title": "The Headless Deskman",   "type": "app",-  "version": "0.1.1",+  "version": "0.2.0",   "bundled": true,-  "files": [-    "headview",-    "install.sh",-    "uninstall.sh",-    "package.json",-    "README.md",-    "SKILL.md"-  ],-  "description": "A browser desktop for a headless Adom container. Runs Xvfb+fluxbox+x11vnc+noVNC inside the container and maps a *.adom.cloud port so any browser (or a Hydrogen webview tab) shows the container's GUI — no SSH tunnel, no local viewer. The container hosts the page itself.",+  "description": "A browser desktop for a headless Adom container. Runs Xvfb+fluxbox+x11vnc+noVNC inside the container and maps a *.adom.cloud port so any browser (or a Hydrogen webview tab) shows the container's GUI — no SSH tunnel, no local viewer. The container hosts the page itself. (Formerly Headview.)",   "tags": [     "cli",     "desktop",@@ -33,9 +25,12 @@     "novnc desktop",     "remote desktop for a container",     "x11 forwarding doesn't work",-    "browser desktop for headless container"+    "browser desktop for headless container",+    "headless deskman",+    "deskman",+    "headview"   ],-  "discovery_pitch": "When a headless container needs a real GUI you can open in a browser (X11 forwarding is blocked by the adom.cloud gateway), reach for headview.",+  "discovery_pitch": "When a headless container needs a real GUI you can open in a browser (X11 forwarding is blocked by the adom.cloud gateway), reach for The Headless Deskman.",   "sample_prompts": [     {       "label": "See a GUI",@@ -53,7 +48,7 @@   "metadata": {     "activate": {       "kind": "skill",-      "skill": "headview",+      "skill": "headless-deskman",       "requires": [],       "verbPrefixes": []     }
page.json+11−8
@@ -1,10 +1,10 @@ {   "slug": "headless-deskman",-  "title": "Headview",+  "title": "The Headless Deskman",   "type": "app",-  "version": "0.1.1",+  "version": "0.2.0",   "bundled": true,-  "description": "A browser desktop for a headless Adom container. Runs Xvfb+fluxbox+x11vnc+noVNC inside the container and maps a *.adom.cloud port so any browser (or a Hydrogen webview tab) shows the container's GUI — no SSH tunnel, no local viewer. The container hosts the page itself.",+  "description": "A browser desktop for a headless Adom container. Runs Xvfb+fluxbox+x11vnc+noVNC inside the container and maps a *.adom.cloud port so any browser (or a Hydrogen webview tab) shows the container's GUI — no SSH tunnel, no local viewer. The container hosts the page itself. (Formerly Headview.)",   "tags": [     "cli",     "desktop",@@ -25,9 +25,12 @@     "novnc desktop",     "remote desktop for a container",     "x11 forwarding doesn't work",-    "browser desktop for headless container"+    "browser desktop for headless container",+    "headless deskman",+    "deskman",+    "headview"   ],-  "discovery_pitch": "When a headless container needs a real GUI you can open in a browser (X11 forwarding is blocked by the adom.cloud gateway), reach for headview.",+  "discovery_pitch": "When a headless container needs a real GUI you can open in a browser (X11 forwarding is blocked by the adom.cloud gateway), reach for The Headless Deskman.",   "sample_prompts": [     {       "label": "See a GUI",@@ -45,7 +48,7 @@   "metadata": {     "activate": {       "kind": "skill",-      "skill": "headview",+      "skill": "headless-deskman",       "requires": [],       "verbPrefixes": []     }@@ -66,11 +69,11 @@     "public": true   },   "files": [-    "headview",+    "headless-deskman",     "install.sh",     "uninstall.sh",     "package.json",     "README.md",     "SKILL.md"   ]-}\ No newline at end of file+}
uninstall.sh+9−7
@@ -1,12 +1,14 @@ #!/bin/sh-# headview uninstall — remove the CLI from PATH.+# The Headless Deskman uninstall — remove the CLI from PATH. set -e for d in /usr/local/bin "$HOME/.local/bin"; do-  if [ -e "$d/headview" ]; then-    if [ -w "$d" ]; then rm -f "$d/headview"-    elif command -v sudo >/dev/null 2>&1; then sudo rm -f "$d/headview"+  for b in headless-deskman headview; do   # headview: pre-0.2.0 name+    if [ -e "$d/$b" ]; then+      if [ -w "$d" ]; then rm -f "$d/$b"+      elif command -v sudo >/dev/null 2>&1; then sudo rm -f "$d/$b"+      fi+      echo "removed $d/$b"     fi-    echo "removed $d/headview"-  fi+  done done-echo "uninstalled headview"+echo "uninstalled headless-deskman"