app
The Headless Deskman
Public Unreviewedby barrett-land
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.)
← Commit history
Rename headview -> The Headless Deskman (headless-deskman); 0.2.0
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. -+++> **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" ]-}+}
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"