aven 0.1.10

Local-first task manager CLI and sync server
Documentation
#!/usr/bin/env bash
set -euo pipefail

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
state_dir="${CUA_SANDBOX_STATE_DIR:-$repo_root/target/cua-sandbox}"
cli_prefix="$state_dir/cli"
playwright_prefix="$state_dir/playwright"
cuabot="$cli_prefix/node_modules/.bin/cuabot"
default_session="${CUA_SANDBOX_SESSION:-}"
custom_image="aven-cuabot:latest"
cuabot_image="trycua/cuabot:latest"
cuabot_version="1.0.14"
default_db="${AVEN_SANDBOX_DB:-$HOME/.local/state/aven/marketing-screenshot.sqlite}"

usage() {
  cat <<'USAGE'
Usage: scripts/cua-sandbox <command> [arguments]

Commands:
  setup                    Install the local CLI and build the persistent image
  start <session>          Start an isolated CuaBot server in the foreground
  stop <session>           Stop a named CuaBot server
  status <session>         Show a named CuaBot server's status
  list                     List discoverable CuaBot sessions
  db-bootstrap <session> [source] [--force]
                           Snapshot a host aven database into a session
  cua <session> [args...]  Pass arguments to a named CuaBot server
  container <session>      Print the Docker container name

Environment:
  CUA_SANDBOX_SESSION    Session fallback when an argument is omitted
  CUA_SANDBOX_STATE_DIR  Host CLI state (default: target/cua-sandbox)
  AVEN_SANDBOX_DB        Database source for db-bootstrap
USAGE
}

require_command() {
  if ! command -v "$1" >/dev/null 2>&1; then
    printf 'missing required command: %s\n' "$1" >&2
    exit 1
  fi
}

require_cli() {
  if [[ ! -x "$cuabot" ]]; then
    echo "CuaBot CLI is missing. Run: scripts/cua-sandbox setup" >&2
    exit 1
  fi
}

require_image() {
  if ! docker image inspect "$custom_image" >/dev/null 2>&1; then
    echo "Sandbox image is missing. Run: scripts/cua-sandbox setup" >&2
    exit 1
  fi
}

activate_image() {
  require_image
  docker tag "$custom_image" "$cuabot_image"
}

resolve_session() {
  local requested="${1:-$default_session}"
  if [[ -z "$requested" ]]; then
    echo "a session name is required" >&2
    echo "Pass one explicitly or set CUA_SANDBOX_SESSION." >&2
    exit 1
  fi
  if [[ ! "$requested" =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]]; then
    echo "invalid session name: $requested" >&2
    exit 1
  fi
  printf '%s\n' "$requested"
}

list_sessions() {
  local found=0
  printf '%-28s %-8s %-7s %s\n' "SESSION" "SERVER" "PORT" "CONTAINER"
  shopt -s nullglob
  for pid_file in "$HOME"/.cuabot/server.*.pid; do
    local filename session pid port state container_state
    filename="${pid_file##*/}"
    session="${filename#server.}"
    session="${session%.pid}"
    pid="$(head -n 1 "$pid_file" 2>/dev/null || true)"
    port="$(cat "$HOME/.cuabot/server.$session.port" 2>/dev/null || true)"
    state="stale"
    if [[ "$pid" =~ ^[0-9]+$ ]] && kill -0 "$pid" 2>/dev/null; then
      state="active"
    fi
    container_state="$(
      docker inspect --format '{{.State.Status}}' "cuabot-xpra-$session" 2>/dev/null \
        || printf 'absent'
    )"
    printf '%-28s %-8s %-7s %s\n' \
      "$session" "$state" "${port:--}" "$container_state"
    found=1
  done
  shopt -u nullglob
  if [[ "$found" -eq 0 ]]; then
    echo "(none)"
  fi
}

bootstrap_database() (
  local session="$1"
  local source="$2"
  local force="$3"
  local container="cuabot-xpra-$session"
  local destination="/home/user/aven-run/aven.db"
  local binary="/home/user/aven-run/aven"

  require_command docker
  require_command sqlite3
  if [[ ! -f "$source" ]]; then
    echo "database source does not exist: $source" >&2
    exit 1
  fi
  if [[ "$(docker inspect --format '{{.State.Status}}' "$container" 2>/dev/null || true)" != "running" ]]; then
    echo "sandbox container is not running: $container" >&2
    exit 1
  fi
  if docker exec "$container" pgrep -f 'aven tui' >/dev/null 2>&1; then
    echo "aven is running in session '$session'; stop it before replacing its database" >&2
    exit 1
  fi
  if ! docker exec -u user "$container" test -x "$binary"; then
    echo "sandbox aven binary is missing: $binary" >&2
    echo "Deploy the branch build before bootstrapping its database." >&2
    exit 1
  fi
  if [[ "$force" != "true" ]] \
    && docker exec -u user "$container" test -e "$destination"
  then
    echo "sandbox database already exists: $destination"
    echo "Pass --force to replace it."
    exit 0
  fi

  local temp_dir snapshot remote
  temp_dir="$(mktemp -d)"
  trap 'rm -rf "$temp_dir"' EXIT
  snapshot="$temp_dir/aven.sqlite"
  remote="/home/user/aven-run/.aven-bootstrap-$session-$$.sqlite"

  sqlite3 "$source" ".backup '$snapshot'"

  local version migration
  local -a normalized=()
  while IFS= read -r version; do
    if [[ ! "$version" =~ ^[0-9]+$ ]]; then
      echo "database contains an invalid migration version: $version" >&2
      exit 1
    fi
    migration="$(compgen -G "$repo_root/migrations/${version}_*.sql" || true)"
    if [[ -z "$migration" ]]; then
      sqlite3 "$snapshot" "DELETE FROM _sqlx_migrations WHERE version = $version;"
      normalized+=("$version")
    fi
  done < <(sqlite3 "$snapshot" 'SELECT version FROM _sqlx_migrations ORDER BY version;')

  if [[ "$(sqlite3 "$snapshot" 'PRAGMA quick_check;')" != "ok" ]]; then
    echo "database snapshot failed its integrity check" >&2
    exit 1
  fi
  chmod 0644 "$snapshot"
  docker exec -u user "$container" mkdir -p /home/user/aven-run
  docker cp "$snapshot" "$container:$remote"
  docker exec -u user "$container" sh -lc "
    set -eu
    cp '$remote' '$destination.bootstrap'
  "
  docker exec "$container" rm -f "$remote"

  if ! docker exec -u user -e AVEN_DB="$destination.bootstrap" \
    "$container" "$binary" list --all --limit 1 >/dev/null
  then
    docker exec -u user "$container" rm -f "$destination.bootstrap"
    echo "sandbox aven could not open the database snapshot" >&2
    exit 1
  fi
  docker exec -u user "$container" \
    rm -f "$destination-wal" "$destination-shm"
  docker exec -u user "$container" mv "$destination.bootstrap" "$destination"

  echo "Bootstrapped $destination"
  echo "Source: $source"
  echo "Session: $session"
  if [[ "${#normalized[@]}" -gt 0 ]]; then
    printf 'Ignored source-only migrations:'
    printf ' %s' "${normalized[@]}"
    printf '\n'
  fi
)

configure_cuabot() {
  local settings="$HOME/.cuabot/settings.json"
  mkdir -p "$(dirname "$settings")"
  SETTINGS_FILE="$settings" node <<'NODE'
const fs = require('fs');
const path = process.env.SETTINGS_FILE;
let settings = {};
try {
  settings = JSON.parse(fs.readFileSync(path, 'utf8'));
} catch (error) {
  if (error.code !== 'ENOENT') throw error;
}
if (settings.telemetryEnabled === undefined) settings.telemetryEnabled = false;
if (settings.aliasIgnored === undefined) settings.aliasIgnored = true;
fs.writeFileSync(path, `${JSON.stringify(settings, null, 2)}\n`);
NODE
}

setup() {
  require_command docker
  require_command node
  require_command npm

  if [[ ! -x /Applications/Xpra.app/Contents/MacOS/Xpra ]]; then
    echo "Xpra is missing. Install it with: brew install --cask xpra" >&2
    exit 1
  fi
  if xattr -p com.apple.quarantine /Applications/Xpra.app >/dev/null 2>&1; then
    echo "Xpra is quarantined. Clear it with:" >&2
    echo "  xattr -cr /Applications/Xpra.app" >&2
    exit 1
  fi
  if ! docker info >/dev/null 2>&1; then
    echo "Docker is unavailable. Start OrbStack and retry." >&2
    exit 1
  fi

  mkdir -p "$state_dir"
  npm install --prefix "$cli_prefix" "cuabot@$cuabot_version"

  local playwright_version
  playwright_version="$(
    node -p \
      "require('$cli_prefix/node_modules/playwright/package.json').version"
  )"
  npm install --prefix "$playwright_prefix" "playwright@$playwright_version"
  "$playwright_prefix/node_modules/.bin/playwright" install chromium

  configure_cuabot
  docker build \
    --file "$repo_root/sandbox/cua/Dockerfile" \
    --tag "$custom_image" \
    "$repo_root/sandbox/cua"
  activate_image

  echo
  echo "Sandbox setup is ready."
  echo "Start one with: scripts/cua-sandbox start <unique-session>"
}

command="${1:-}"
case "$command" in
  setup)
    setup
    ;;
  start)
    require_cli
    session="$(resolve_session "${2:-}")"
    pid_file="$HOME/.cuabot/server.$session.pid"
    if [[ -f "$pid_file" ]]; then
      pid="$(head -n 1 "$pid_file" 2>/dev/null || true)"
      if [[ "$pid" =~ ^[0-9]+$ ]] && kill -0 "$pid" 2>/dev/null; then
        echo "session '$session' is already active under pid $pid" >&2
        echo "Use a different session or stop this one explicitly." >&2
        exit 1
      fi
    fi
    activate_image
    export XPRA_USER_CONF_DIRS="$repo_root/sandbox/cua/xpra"
    export XPRA_OSX_SHOW_MENU_DEFAULT=0
    export XPRA_OSX_USE_WINDOW_MENU=0
    exec "$cuabot" -n "$session" --serve
    ;;
  stop)
    require_cli
    session="$(resolve_session "${2:-}")"
    exec "$cuabot" -n "$session" --stop
    ;;
  status)
    require_cli
    session="$(resolve_session "${2:-}")"
    exec "$cuabot" -n "$session" --status
    ;;
  list)
    require_command docker
    list_sessions
    ;;
  db-bootstrap)
    session="$(resolve_session "${2:-}")"
    source="${3:-$default_db}"
    force="false"
    if [[ "$source" == "--force" ]]; then
      source="$default_db"
      force="true"
    elif [[ "${4:-}" == "--force" ]]; then
      force="true"
    elif [[ -n "${4:-}" ]]; then
      echo "unexpected argument: ${4}" >&2
      exit 1
    fi
    bootstrap_database "$session" "$source" "$force"
    ;;
  cua)
    require_cli
    session="$(resolve_session "${2:-}")"
    shift 2
    exec "$cuabot" -n "$session" "$@"
    ;;
  container)
    session="$(resolve_session "${2:-}")"
    printf 'cuabot-xpra-%s\n' "$session"
    ;;
  help|-h|--help|'')
    usage
    ;;
  *)
    echo "unknown command: $command" >&2
    usage >&2
    exit 1
    ;;
esac