hermes-agent/scripts/dev-sandbox.sh
ethernet 5e849942c3 feat(dev): add isolated sandbox script for local dev
scripts/desktop-sandbox.sh runs a Hermes desktop instance in an isolated
sandbox — separate HERMES_HOME, separate Electron userData, and a
distinct
app name (HERMES_DESKTOP_APP_NAME) so it doesn't compete with the main
desktop instance's single-instance lock.

Two modes:
- Ephemeral (default): temp dir, cleaned up on exit
- --persistent: stored under .hermes-sandbox/ in the worktree git root,
  survives restarts for repeat testing

In the Nix devShell the script is available as 'sandbox'.

Also makes APP_NAME overridable via HERMES_DESKTOP_APP_NAME in main.ts —
app.setName() runs before requestSingleInstanceLock(), so the overridden
name changes the lock key. collectRelaunchEnv already preserves
HERMES_DESKTOP_* vars through self-update relaunches; test updated to
cover the new env var.
2026-07-10 18:51:49 -04:00

152 lines
4.3 KiB
Bash
Executable file

#!/usr/bin/env bash
# Run a Hermes instance in an isolated sandbox — separate HERMES_HOME,
# separate Electron userData, and a distinct Desktop app name so it doesn't compete
# with your main desktop instance's single-instance lock.
#
# By default the sandbox is throwaway: a temp dir is created and removed on
# exit. Use --persistent to keep the sandbox across restarts (stored under
# .hermes-sandbox/ in the worktree git root).
#
# Usage:
# scripts/dev-sandbox.sh python -m hermes_cli.main
# scripts/dev-sandbox.sh hermes desktop
# scripts/dev-sandbox.sh electron .
# scripts/dev-sandbox.sh -- npm run dev # from apps/desktop/
# scripts/dev-sandbox.sh --persistent hermes desktop
# scripts/dev-sandbox.sh --persistent -- npm run dev
#
# Override the app name (default: HermesSandbox):
# HERMES_DEV_SANDBOX_NAME=Staging scripts/dev-sandbox.sh hermes desktop
#
# Override the persistent sandbox dir name (default: .hermes-sandbox):
# HERMES_DEV_SANDBOX_DIR=.staging-sandbox scripts/dev-sandbox.sh --persistent hermes desktop
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
print_help() {
cat <<'EOF'
Usage: dev-sandbox.sh [--persistent] [--] <command...>
Run a Hermes instance in an isolated sandbox.
Options:
--persistent Keep the sandbox dir across restarts (under the worktree
git root, in .hermes-sandbox/). Without this flag the
sandbox is a temp dir that is removed on exit.
--delete Delete the existing persistent sandbox in .hermes-sandbox.
-h, --help Show this help message.
Environment:
HERMES_DEV_SANDBOX_NAME Override the app name (default: HermesSandbox)
HERMES_DEV_SANDBOX_DIR Override the persistent dir name (default: .hermes-sandbox)
Examples:
dev-sandbox.sh hermes desktop
dev-sandbox.sh --persistent hermes desktop
dev-sandbox.sh -- npm run dev
EOF
}
PERSISTENT=false
DELETE=false
while [ "$#" -gt 0 ]; do
case "$1" in
--persistent)
PERSISTENT=true
shift
;;
--delete)
DELETE=true
shift
;;
-h|--help)
print_help
exit 0
;;
--)
shift
break
;;
*)
break
;;
esac
done
if [ "$#" -eq 0 ]; then
print_help >&2
exit 1
fi
SANDBOX_DIR_NAME="${HERMES_DEV_SANDBOX_DIR:-.hermes-sandbox}"
GIT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR/..")"
GIT_ROOT="$(cd "$GIT_ROOT" && pwd)"
PERSISTENT_SANDBOX_ROOT="$GIT_ROOT/$SANDBOX_DIR_NAME"
if [ "$DELETE" = true ]; then
if [ -d "$PERSISTENT_SANDBOX_ROOT" ]; then
read -r -p "[sandbox] delete $PERSISTENT_SANDBOX_ROOT? [y/N] " REPLY
case "$REPLY" in
[yY]|[yY][eE][sS])
echo "[sandbox] deleting $PERSISTENT_SANDBOX_ROOT" >&2
rm -rf -- "$PERSISTENT_SANDBOX_ROOT"
;;
*)
echo "[sandbox] aborted" >&2
exit 1
;;
esac
else
echo "[sandbox] nothing to delete at $PERSISTENT_SANDBOX_ROOT" >&2
fi
exit 0
fi
# Derive a per-worktree app name so multiple checkouts don't collide.
# Each worktree has its own toplevel path even though they share one repo,
# so we hash that path into a short, stable suffix.
WORKTREE_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR/..")"
WORKTREE_ROOT="$(cd "$WORKTREE_ROOT" && pwd)"
WORKTREE_HASH="$(printf '%s' "$WORKTREE_ROOT" | cksum | cut -d' ' -f1)"
WORKTREE_NAME="$(basename "$WORKTREE_ROOT")"
DEFAULT_SANDBOX_NAME="HermesSandbox-${WORKTREE_NAME}-${WORKTREE_HASH}"
SANDBOX_NAME="${HERMES_DEV_SANDBOX_NAME:-$DEFAULT_SANDBOX_NAME}"
if [ "$PERSISTENT" = true ]; then
SANDBOX_ROOT="$PERSISTENT_SANDBOX_ROOT"
else
SANDBOX_ROOT="$(mktemp -d -t hermes-sandbox.XXXXXX)"
fi
export HERMES_HOME="$SANDBOX_ROOT/hermes-home"
export HERMES_DESKTOP_USER_DATA_DIR="$SANDBOX_ROOT/user-data"
export HERMES_DESKTOP_APP_NAME="$SANDBOX_NAME"
mkdir -p "$HERMES_HOME" "$HERMES_DESKTOP_USER_DATA_DIR"
echo "[sandbox] HERMES_HOME=$HERMES_HOME" >&2
echo "[sandbox] userData=$HERMES_DESKTOP_USER_DATA_DIR" >&2
echo "[sandbox] appName=$HERMES_DESKTOP_APP_NAME" >&2
if [ "$PERSISTENT" = true ]; then
echo "[sandbox] persistent: $SANDBOX_ROOT" >&2
else
echo "[sandbox] ephemeral (will be cleaned up on exit)" >&2
fi
if [ "$PERSISTENT" = false ]; then
cleanup() {
chmod -R u+w "$SANDBOX_ROOT"
rm -rf -- "$SANDBOX_ROOT"
}
trap cleanup EXIT
trap 'cleanup; exit 130' INT TERM
fi
"$@"
rc=$?
exit $rc