diff --git a/CLAUDE.md b/CLAUDE.md index 5ecc23a..3a71a3b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,7 +13,7 @@ The goal is that `docker compose up` gives you a working setup where Claude Desk ## Status -Early stage. What exists: a single image (`Dockerfile`, `launch-apps.sh`) with Claude Desktop, Ghidra (plus GhidraMCP and an ESP32 loader), binwalk, unblob, esptool and REA, started by `run.sh` as two containers (Claude Desktop + Ghidra, and the REA MCP server); `README.md` covers usage; Claude Desktop's MCP config is `claude-desktop/claude_desktop_config.json`; Ghidra config files are in `ghidra/`. No compose setup yet. The layout below is the **intended** structure. Update this file when the real structure differs. +Early stage. What exists: a single image (`Dockerfile`, `launch-apps.sh`) with Claude Desktop, Ghidra (plus GhidraMCP and an ESP32 loader), binwalk, unblob, esptool and REA, started by `run.sh` (or the app-menu launcher `install.sh` creates: `~/.local/share/applications/claude-re-setup.desktop`, icons taken from the image, `Exec` pointing at this checkout's `run.sh`; `run.sh` shows errors in a zenity dialog when it has no terminal) as two containers (Claude Desktop + Ghidra, and the REA MCP server); `README.md` covers usage; Claude Desktop's MCP config is `claude-desktop/claude_desktop_config.json`; Ghidra config files are in `ghidra/`. No compose setup yet. The layout below is the **intended** structure. Update this file when the real structure differs. ## Intended layout diff --git a/README.md b/README.md index 01d1c3e..4d1550a 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ Docker and an X server (or XWayland) is needed on the host. ## Usage ```bash +./install.sh # build the image and add "Claude RE Workstation" to the app menu +./install.sh --uninstall # remove that launcher again ./run.sh # build the image if needed, pick a folder to share, start ./run.sh --build # rebuild the image first ./run.sh --workspace ~/re/target-x # share this folder without asking @@ -18,6 +20,9 @@ The shared folder is mounted at `/workspace` in both containers. Sign in to Claude Desktop on every start; the sign-in is not kept. Closing Claude Desktop or Ghidra's project window stops everything. +The launcher runs `run.sh` from this checkout, so re-run `./install.sh` after +moving it. Started from the menu, errors appear in a dialog. + ## What runs where | Container | Contents | Network | diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..a66adf8 --- /dev/null +++ b/install.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# Build the image and add a "Claude RE Workstation" launcher to the desktop's +# application menu. The launcher runs ./run.sh from this checkout, so keep +# the checkout where it is (re-run this script after moving it). +# +# Usage: ./install.sh [--uninstall] +# --uninstall remove the launcher and its icons (the image is kept; +# remove it with: docker rmi claude-re-setup:latest) +# +# RUNTIME=podman is passed on to the launcher. + +set -euo pipefail + +IMAGE="${IMAGE:-claude-re-setup:latest}" +RUNTIME="${RUNTIME:-docker}" + +app_id="claude-re-setup" +script_dir="$(dirname "$(readlink -f "$0")")" +data_home="${XDG_DATA_HOME:-$HOME/.local/share}" +desktop_file="$data_home/applications/$app_id.desktop" +icon_root="$data_home/icons/hicolor" +icon_sizes=(16 32 48 128 256) + +die() { echo "error: $*" >&2; exit 1; } +info() { echo "==> $*"; } + +refresh_caches() { + command -v update-desktop-database >/dev/null \ + && update-desktop-database -q "$data_home/applications" || true + command -v gtk-update-icon-cache >/dev/null && [[ -f "$icon_root/index.theme" ]] \ + && gtk-update-icon-cache -q -t "$icon_root" || true +} + +case "${1:-}" in + --uninstall) + rm -f "$desktop_file" + for size in "${icon_sizes[@]}"; do + rm -f "$icon_root/${size}x${size}/apps/$app_id.png" + done + refresh_caches + info "Removed the launcher. The image is still there: $RUNTIME rmi $IMAGE" + exit 0 ;; + -h|--help) sed -n '2,10p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + "") ;; + *) die "unknown argument: $1" ;; +esac + +# The desktop entry quotes the path; these characters would need escaping +# there, so such a checkout location is refused instead. +[[ "$script_dir" != *[\"\`\$\\%]* ]] \ + || die "the checkout path contains a character a .desktop file can't hold: $script_dir" + +# --- Image ------------------------------------------------------------------- +info "Building $IMAGE (cached layers are reused)" +RUNTIME="$RUNTIME" IMAGE="$IMAGE" "$script_dir/run.sh" --build-only + +# --- Icons ------------------------------------------------------------------- +# Claude Desktop's own icons, taken from the image, under this launcher's +# name so they don't clash with a natively installed Claude Desktop. +info "Installing icons" +cid="$("$RUNTIME" create "$IMAGE")" +trap '"$RUNTIME" rm "$cid" >/dev/null 2>&1 || true' EXIT +for size in "${icon_sizes[@]}"; do + dir="$icon_root/${size}x${size}/apps" + mkdir -p "$dir" + "$RUNTIME" cp "$cid:/usr/share/icons/hicolor/${size}x${size}/apps/claude-desktop.png" \ + "$dir/$app_id.png" +done + +# --- Desktop entry ----------------------------------------------------------- +exec_line="\"$script_dir/run.sh\"" +[[ "$RUNTIME" != docker ]] && exec_line="env RUNTIME=$RUNTIME $exec_line" + +info "Writing $desktop_file" +mkdir -p "$(dirname "$desktop_file")" +cat > "$desktop_file" </dev/null; then + desktop-file-validate "$desktop_file" +fi +refresh_caches + +info "Done. Start \"Claude RE Workstation\" from the application menu." diff --git a/run.sh b/run.sh index f0e244d..24a6575 100755 --- a/run.sh +++ b/run.sh @@ -6,8 +6,9 @@ # Also starts the REA MCP server in a second, isolated container and stops it # when Claude Desktop's container ends. # -# Usage: ./run.sh [--build] [--check-only] [--workspace DIR] +# Usage: ./run.sh [--build] [--build-only] [--check-only] [--workspace DIR] # --build rebuild the image even if it exists +# --build-only (re)build the image and exit # --check-only run the display check and exit without starting Claude # --workspace DIR share DIR without asking (also: WORKSPACE=DIR) @@ -27,23 +28,50 @@ last_workspace_file="$state_dir/last_workspace" build=0 check_only=0 +build_only=0 while (( $# )); do case "$1" in --build) build=1 ;; + --build-only) build=1 build_only=1 ;; --check-only) check_only=1 ;; --workspace) [[ $# -ge 2 ]] || { echo "--workspace needs a directory" >&2; exit 2; } WORKSPACE="$2"; shift ;; --workspace=*) WORKSPACE="${1#*=}" ;; - -h|--help) sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + -h|--help) sed -n '2,13p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; *) echo "Unknown argument: $1" >&2; exit 2 ;; esac shift done -die() { echo "error: $*" >&2; exit 1; } +# Started from the app menu (install.sh) there is no terminal to read +# errors in, so they are also shown in a dialog. +die() { + echo "error: $*" >&2 + if [[ ! -t 2 ]] && command -v zenity >/dev/null; then + zenity --error --no-wrap --title="Claude RE workstation" --text="$*" 2>/dev/null || true + fi + exit 1 +} info() { echo "==> $*"; } command -v "$RUNTIME" >/dev/null || die "'$RUNTIME' not found (set RUNTIME=podman to use podman)" + +# --- Image build ------------------------------------------------------------- +ensure_image() { + if (( build )) || ! "$RUNTIME" image inspect "$IMAGE" >/dev/null 2>&1; then + info "Building $IMAGE" + "$RUNTIME" build \ + --build-arg USER_UID="$(id -u)" \ + --build-arg USER_GID="$(id -g)" \ + -t "$IMAGE" "$script_dir" + fi +} +if (( build_only )); then + ensure_image + info "Built $IMAGE" + exit 0 +fi + command -v xauth >/dev/null || die "xauth not found on the host (dnf install xorg-x11-xauth)" # --- Display ----------------------------------------------------------------- @@ -69,13 +97,7 @@ xauth nlist "$DISPLAY" | sed -e 's/^..../ffff/' | xauth -f "$xauth_file" nmerge chmod 600 "$xauth_file" # --- Image ------------------------------------------------------------------- -if (( build )) || ! "$RUNTIME" image inspect "$IMAGE" >/dev/null 2>&1; then - info "Building $IMAGE" - "$RUNTIME" build \ - --build-arg USER_UID="$(id -u)" \ - --build-arg USER_GID="$(id -g)" \ - -t "$IMAGE" "$script_dir" -fi +ensure_image # label=disable: the X socket and cookie are host files that SELinux would # otherwise block, and relabeling /tmp/.X11-unix with :z would break the host.