Загрузить файлы в «/»
This commit is contained in:
+103
@@ -0,0 +1,103 @@
|
||||
# Agent Sandbox — isolated Linux environment for CLI coding agents
|
||||
# Designed for macOS hosts (Docker Desktop / OrbStack)
|
||||
FROM ubuntu:24.04
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG USERNAME=agent
|
||||
ARG USER_UID=1000
|
||||
ARG USER_GID=1000
|
||||
|
||||
LABEL org.opencontainers.image.title="asandbox" \
|
||||
org.opencontainers.image.description="Secure Docker sandbox for CLI AI coding agents" \
|
||||
org.opencontainers.image.source="local"
|
||||
|
||||
# Base tools
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
wget \
|
||||
git \
|
||||
gnupg \
|
||||
openssh-client \
|
||||
sudo \
|
||||
less \
|
||||
nano \
|
||||
vim-tiny \
|
||||
jq \
|
||||
unzip \
|
||||
zip \
|
||||
tar \
|
||||
gzip \
|
||||
bzip2 \
|
||||
xz-utils \
|
||||
build-essential \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
python3-dev \
|
||||
ripgrep \
|
||||
fd-find \
|
||||
tree \
|
||||
rsync \
|
||||
locales \
|
||||
tzdata \
|
||||
procps \
|
||||
iproute2 \
|
||||
iputils-ping \
|
||||
dnsutils \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& ln -sf /usr/bin/fdfind /usr/local/bin/fd \
|
||||
&& locale-gen en_US.UTF-8
|
||||
|
||||
ENV LANG=en_US.UTF-8 \
|
||||
LC_ALL=en_US.UTF-8 \
|
||||
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
||||
PIP_NO_CACHE_DIR=1 \
|
||||
NPM_CONFIG_UPDATE_NOTIFIER=false \
|
||||
NPM_CONFIG_FUND=false
|
||||
|
||||
# Node.js 22 LTS (many agents are Node-based)
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
||||
&& apt-get install -y --no-install-recommends nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& corepack enable || true
|
||||
|
||||
# uv (fast Python package manager)
|
||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh \
|
||||
&& mv /root/.local/bin/uv /usr/local/bin/uv \
|
||||
&& mv /root/.local/bin/uvx /usr/local/bin/uvx 2>/dev/null || true
|
||||
|
||||
# Create non-root user matching host UID/GID when possible (macOS staff=20 often exists as dialout)
|
||||
RUN set -eux; \
|
||||
if getent group "${USER_GID}" >/dev/null; then \
|
||||
EXISTING_GROUP="$(getent group "${USER_GID}" | cut -d: -f1)"; \
|
||||
useradd --uid "${USER_UID}" --gid "${USER_GID}" -m -s /bin/bash "${USERNAME}" \
|
||||
|| useradd --uid "${USER_UID}" -g "${EXISTING_GROUP}" -m -s /bin/bash "${USERNAME}"; \
|
||||
else \
|
||||
groupadd --gid "${USER_GID}" "${USERNAME}"; \
|
||||
useradd --uid "${USER_UID}" --gid "${USER_GID}" -m -s /bin/bash "${USERNAME}"; \
|
||||
fi; \
|
||||
echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" > "/etc/sudoers.d/${USERNAME}"; \
|
||||
chmod 0440 "/etc/sudoers.d/${USERNAME}"
|
||||
|
||||
# installagent + shell integration (agents installed on demand inside shell)
|
||||
COPY installagent /usr/local/bin/installagent
|
||||
COPY entrypoint.sh /usr/local/bin/asandbox-entrypoint
|
||||
COPY bashrc.asandbox /etc/asandbox.bashrc
|
||||
RUN chmod +x /usr/local/bin/installagent /usr/local/bin/asandbox-entrypoint \
|
||||
&& echo 'source /etc/asandbox.bashrc' >> /home/${USERNAME}/.bashrc \
|
||||
&& echo 'source /etc/asandbox.bashrc' >> /home/${USERNAME}/.profile \
|
||||
&& mkdir -p /home/${USERNAME}/.local/bin /home/${USERNAME}/.npm-global/bin \
|
||||
&& chown -R ${USERNAME} /home/${USERNAME}
|
||||
|
||||
# Workspace mount point (host path is also bind-mounted at real path)
|
||||
RUN mkdir -p /workspace && chown ${USERNAME} /workspace
|
||||
|
||||
WORKDIR /workspace
|
||||
USER ${USERNAME}
|
||||
ENV HOME=/home/${USERNAME} \
|
||||
PATH="/home/${USERNAME}/.local/bin:/home/${USERNAME}/.npm-global/bin:/usr/local/bin:${PATH}"
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/asandbox-entrypoint"]
|
||||
CMD ["bash", "-l"]
|
||||
@@ -0,0 +1,670 @@
|
||||
#!/usr/bin/env bash
|
||||
# asandbox — run any CLI coding agent inside Docker with host FS integration
|
||||
# Works on macOS (Docker Desktop / OrbStack) and Linux.
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="1.1.0"
|
||||
# Program root = folder where this script lives (e.g. /Users/macbook/asandbox)
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
IMAGE_NAME="${ASANDBOX_IMAGE:-asandbox:latest}"
|
||||
SHARE_DIR="${ASANDBOX_SHARE:-${ROOT_DIR}}"
|
||||
CONFIG_DIR="${ASANDBOX_CONFIG_DIR:-${ROOT_DIR}/config}"
|
||||
CONFIG_FILE="${CONFIG_DIR}/config.env"
|
||||
DEFAULT_NETWORK="${ASANDBOX_NETWORK:-bridge}"
|
||||
DEFAULT_MEMORY="${ASANDBOX_MEMORY:-4g}"
|
||||
DEFAULT_CPUS="${ASANDBOX_CPUS:-4}"
|
||||
|
||||
# ── colors ──────────────────────────────────────────────────────────
|
||||
if [[ -t 2 ]]; then
|
||||
C_RED=$'\033[31m'; C_GRN=$'\033[32m'; C_YLW=$'\033[33m'
|
||||
C_BLU=$'\033[34m'; C_DIM=$'\033[2m'; C_RST=$'\033[0m'; C_BLD=$'\033[1m'
|
||||
else
|
||||
C_RED=; C_GRN=; C_YLW=; C_BLU=; C_DIM=; C_RST=; C_BLD=
|
||||
fi
|
||||
|
||||
die() { echo "${C_RED}asandbox:${C_RST} $*" >&2; exit 1; }
|
||||
info() { echo "${C_BLU}asandbox:${C_RST} $*" >&2; }
|
||||
ok() { echo "${C_GRN}asandbox:${C_RST} $*" >&2; }
|
||||
warn() { echo "${C_YLW}asandbox:${C_RST} $*" >&2; }
|
||||
|
||||
# ── usage ───────────────────────────────────────────────────────────
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
asandbox — run CLI AI agents in Docker with host filesystem integration
|
||||
|
||||
USAGE
|
||||
asandbox <command> [options] [--] [agent-args...]
|
||||
|
||||
COMMANDS
|
||||
run <agent|cmd...> Run an agent or arbitrary command in the sandbox
|
||||
shell Interactive bash inside the sandbox
|
||||
installagent <name> Install an agent inside the sandbox (claude, cursor, …)
|
||||
build Build (or rebuild) the sandbox Docker image
|
||||
agents List known agent presets / installagent catalog
|
||||
config Show / create config
|
||||
doctor Check Docker, image, mounts
|
||||
version Print version
|
||||
help Show this help
|
||||
|
||||
OPTIONS (run / shell)
|
||||
-w, --workdir DIR Host directory to mount (default: git root or cwd)
|
||||
-n, --name NAME Container name (default: auto)
|
||||
--network MODE none | bridge | host (default: bridge)
|
||||
--memory SIZE Memory limit (default: 4g)
|
||||
--cpus N CPU limit (default: 4)
|
||||
--read-only Read-only root filesystem (+tmpfs /tmp)
|
||||
--no-mount-home Do not mount agent config dirs from host home
|
||||
--mount-ssh Mount ~/.ssh (read-only) for git over SSH
|
||||
--mount-gitconfig Mount ~/.gitconfig (read-only)
|
||||
--mount PATH[:CONT] Extra bind mount (repeatable). CONT defaults to PATH
|
||||
--env KEY=VAL Pass env var (repeatable)
|
||||
--env-file FILE Pass env file
|
||||
--pass-env KEY Pass host env KEY if set (repeatable)
|
||||
--image NAME Override image (default: asandbox:latest)
|
||||
--rm / --keep Remove container on exit (default) / keep it
|
||||
--detach, -d Run in background
|
||||
-q, --quiet Less noise
|
||||
--dry-run Print docker command and exit
|
||||
|
||||
AGENT PRESETS / installagent
|
||||
claude, codex, gemini, antigravity (agy), cursor (agent),
|
||||
opencode, aider, qwen, amp, goose, continue, bash
|
||||
Inside shell: installagent list | installagent claude
|
||||
Or any command: asandbox run -- node script.js
|
||||
|
||||
EXAMPLES
|
||||
asandbox build
|
||||
asandbox shell
|
||||
asandbox installagent list
|
||||
asandbox installagent claude
|
||||
asandbox installagent cursor
|
||||
asandbox installagent antigravity
|
||||
asandbox installagent opencode
|
||||
asandbox run claude
|
||||
asandbox run claude "explain this project"
|
||||
asandbox run -w ~/proj codex
|
||||
asandbox run --network none --read-only -- rg "TODO"
|
||||
asandbox run --mount-ssh -- git pull
|
||||
asandbox run --env ANTHROPIC_API_KEY=sk-... claude
|
||||
|
||||
SECURITY
|
||||
• No Docker socket is mounted (agents cannot escape via Docker).
|
||||
• Runs as non-root user inside the container.
|
||||
• Capabilities dropped; no-new-privileges enabled.
|
||||
• Host project directory is bind-mounted (same absolute path on Mac).
|
||||
• Agent configs (~/.claude, ~/.codex, …) mounted so auth/state persist.
|
||||
• Use --network none for fully offline runs when possible.
|
||||
|
||||
CONFIG
|
||||
Program folder: same directory as this script (default ~/asandbox)
|
||||
config/config.env optional defaults
|
||||
Dockerfile, installagent, persist/ live next to the script
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
# ── config ──────────────────────────────────────────────────────────
|
||||
load_config() {
|
||||
if [[ -f "$CONFIG_FILE" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
set -a; source "$CONFIG_FILE"; set +a
|
||||
fi
|
||||
}
|
||||
|
||||
write_default_config() {
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
if [[ -f "$CONFIG_FILE" ]]; then
|
||||
info "config already exists: $CONFIG_FILE"
|
||||
return 0
|
||||
fi
|
||||
cat > "$CONFIG_FILE" <<'EOF'
|
||||
# asandbox defaults (bash-compatible env file)
|
||||
# ASANDBOX_IMAGE=asandbox:latest
|
||||
# ASANDBOX_NETWORK=bridge
|
||||
# ASANDBOX_MEMORY=4g
|
||||
# ASANDBOX_CPUS=4
|
||||
# ASANDBOX_MOUNT_SSH=0
|
||||
# ASANDBOX_MOUNT_GITCONFIG=1
|
||||
# ASANDBOX_QUIET=0
|
||||
|
||||
# Extra env keys always forwarded when set on host:
|
||||
# ASANDBOX_PASS_ENV="ANTHROPIC_API_KEY OPENAI_API_KEY GEMINI_API_KEY GOOGLE_API_KEY XAI_API_KEY"
|
||||
EOF
|
||||
ok "wrote $CONFIG_FILE"
|
||||
}
|
||||
|
||||
# ── helpers ─────────────────────────────────────────────────────────
|
||||
require_docker() {
|
||||
command -v docker >/dev/null 2>&1 || die "docker not found. Install Docker Desktop or OrbStack."
|
||||
docker info >/dev/null 2>&1 || die "Docker daemon not running. Start Docker Desktop / OrbStack."
|
||||
}
|
||||
|
||||
image_exists() {
|
||||
docker image inspect "$IMAGE_NAME" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
resolve_workdir() {
|
||||
local dir="${1:-}"
|
||||
if [[ -n "$dir" ]]; then
|
||||
(cd "$dir" && pwd -P)
|
||||
return
|
||||
fi
|
||||
# Prefer git root when inside a repo
|
||||
if git rev-parse --show-toplevel >/dev/null 2>&1; then
|
||||
git rev-parse --show-toplevel
|
||||
return
|
||||
fi
|
||||
pwd -P
|
||||
}
|
||||
|
||||
# Map agent name → argv inside container
|
||||
resolve_agent_cmd() {
|
||||
local agent="$1"
|
||||
shift || true
|
||||
case "$agent" in
|
||||
claude|claude-code)
|
||||
echo "claude"; printf '%s\n' "$@"
|
||||
;;
|
||||
codex|openai-codex)
|
||||
echo "codex"; printf '%s\n' "$@"
|
||||
;;
|
||||
gemini|gemini-cli)
|
||||
echo "gemini"; printf '%s\n' "$@"
|
||||
;;
|
||||
antigravity|agy|antigravity-cli)
|
||||
echo "agy"; printf '%s\n' "$@"
|
||||
;;
|
||||
cursor|cursor-cli|cursor-agent)
|
||||
echo "agent"; printf '%s\n' "$@"
|
||||
;;
|
||||
opencode|open-code)
|
||||
echo "opencode"; printf '%s\n' "$@"
|
||||
;;
|
||||
aider|aider-chat)
|
||||
echo "aider"; printf '%s\n' "$@"
|
||||
;;
|
||||
qwen|qwen-code)
|
||||
echo "qwen"; printf '%s\n' "$@"
|
||||
;;
|
||||
amp)
|
||||
echo "amp"; printf '%s\n' "$@"
|
||||
;;
|
||||
goose)
|
||||
echo "goose"; printf '%s\n' "$@"
|
||||
;;
|
||||
continue|cn)
|
||||
echo "cn"; printf '%s\n' "$@"
|
||||
;;
|
||||
installagent|install-agent)
|
||||
echo "installagent"; printf '%s\n' "$@"
|
||||
;;
|
||||
bash|sh|shell)
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "bash"; echo "-l"
|
||||
else
|
||||
echo "bash"; echo "-lc"; echo "$*"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
# Treat first token as command
|
||||
echo "$agent"; printf '%s\n' "$@"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Known host config dirs to bind-mount (host → container path under $HOME)
|
||||
agent_home_mounts() {
|
||||
# format: host_relpath
|
||||
cat <<'EOF'
|
||||
.claude
|
||||
.codex
|
||||
.config/gh
|
||||
.gemini
|
||||
.grok
|
||||
.cursor
|
||||
.local/share/claude
|
||||
.npm
|
||||
.antigravity
|
||||
.agy
|
||||
.config/opencode
|
||||
.config/aider
|
||||
EOF
|
||||
}
|
||||
|
||||
# Persistent install roots (survive --rm containers)
|
||||
ensure_persist_dirs() {
|
||||
mkdir -p \
|
||||
"${SHARE_DIR}/persist/.local/bin" \
|
||||
"${SHARE_DIR}/persist/.npm-global/bin" \
|
||||
"${SHARE_DIR}/persist/.cache"
|
||||
}
|
||||
|
||||
# Env keys commonly needed by agents
|
||||
default_pass_env_keys() {
|
||||
echo "ANTHROPIC_API_KEY OPENAI_API_KEY OPENAI_BASE_URL GEMINI_API_KEY GOOGLE_API_KEY GOOGLE_GENAI_API_KEY XAI_API_KEY GROK_API_KEY CURSOR_API_KEY GITHUB_TOKEN GH_TOKEN TERM COLORTERM LANG LC_ALL EDITOR"
|
||||
}
|
||||
|
||||
# ── build ───────────────────────────────────────────────────────────
|
||||
cmd_build() {
|
||||
require_docker
|
||||
local no_cache=0 force=0
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--no-cache) no_cache=1; shift ;;
|
||||
--force|-f) force=1; shift ;;
|
||||
-h|--help) echo "Usage: asandbox build [--no-cache] [--force]"; return 0 ;;
|
||||
*) die "unknown build option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -f "$SHARE_DIR/Dockerfile" ]] || die "Dockerfile not found at $SHARE_DIR/Dockerfile"
|
||||
|
||||
local args=(build -t "$IMAGE_NAME" -f "$SHARE_DIR/Dockerfile" "$SHARE_DIR")
|
||||
if [[ $no_cache -eq 1 ]]; then
|
||||
args+=(--no-cache)
|
||||
fi
|
||||
|
||||
# Pass host UID/GID so files created in mounts match macOS user
|
||||
args+=(--build-arg "USER_UID=$(id -u)" --build-arg "USER_GID=$(id -g)")
|
||||
|
||||
info "building image ${C_BLD}${IMAGE_NAME}${C_RST} from $SHARE_DIR"
|
||||
docker "${args[@]}"
|
||||
ok "image ready: $IMAGE_NAME"
|
||||
}
|
||||
|
||||
# ── doctor ──────────────────────────────────────────────────────────
|
||||
cmd_doctor() {
|
||||
echo "${C_BLD}asandbox doctor${C_RST} (v$VERSION)"
|
||||
echo
|
||||
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
ok "docker CLI: $(command -v docker)"
|
||||
else
|
||||
die "docker CLI missing"
|
||||
fi
|
||||
|
||||
if docker info >/dev/null 2>&1; then
|
||||
local ctx
|
||||
ctx=$(docker context show 2>/dev/null || echo unknown)
|
||||
ok "docker daemon: running (context: $ctx)"
|
||||
else
|
||||
die "docker daemon not running"
|
||||
fi
|
||||
|
||||
if image_exists; then
|
||||
ok "image: $IMAGE_NAME present"
|
||||
else
|
||||
warn "image: $IMAGE_NAME missing — run: asandbox build"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "paths:"
|
||||
echo " share: $SHARE_DIR"
|
||||
echo " config: $CONFIG_FILE"
|
||||
echo " home: $HOME"
|
||||
echo " cwd: $(pwd -P)"
|
||||
if git rev-parse --show-toplevel >/dev/null 2>&1; then
|
||||
echo " git: $(git rev-parse --show-toplevel)"
|
||||
fi
|
||||
echo
|
||||
echo "platform: $(uname -s)/$(uname -m)"
|
||||
echo "uid/gid: $(id -u):$(id -g)"
|
||||
|
||||
if [[ -f "$SHARE_DIR/Dockerfile" ]]; then
|
||||
ok "Dockerfile found"
|
||||
else
|
||||
warn "Dockerfile missing at $SHARE_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── agents list ─────────────────────────────────────────────────────
|
||||
cmd_agents() {
|
||||
cat <<'EOF'
|
||||
Agent presets (run via asandbox <name> or asandbox run <name>):
|
||||
|
||||
claude Anthropic Claude Code
|
||||
codex OpenAI Codex CLI
|
||||
gemini Google Gemini CLI
|
||||
antigravity Google Antigravity CLI (binary: agy)
|
||||
cursor Cursor Agent CLI (binary: agent)
|
||||
opencode OpenCode
|
||||
aider Aider
|
||||
qwen Qwen Code
|
||||
amp Sourcegraph Amp
|
||||
goose Block Goose
|
||||
continue Continue CLI (binary: cn)
|
||||
bash Interactive shell
|
||||
|
||||
Install inside sandbox (persisted under <program>/persist):
|
||||
|
||||
asandbox installagent list
|
||||
asandbox installagent claude
|
||||
asandbox installagent cursor
|
||||
asandbox installagent antigravity
|
||||
asandbox installagent opencode
|
||||
asandbox installagent all
|
||||
|
||||
# or inside shell:
|
||||
asandbox shell
|
||||
$ installagent list
|
||||
$ installagent gemini
|
||||
|
||||
Host config dirs auto-mounted (if present):
|
||||
~/.claude ~/.codex ~/.gemini ~/.grok ~/.cursor ~/.config/gh
|
||||
~/.antigravity ~/.agy ~/.config/opencode
|
||||
EOF
|
||||
}
|
||||
|
||||
# ── installagent (host wrapper → container) ─────────────────────────
|
||||
cmd_installagent() {
|
||||
if [[ $# -eq 0 ]]; then
|
||||
set -- list
|
||||
fi
|
||||
# Run installagent inside sandbox with network (needed to download)
|
||||
cmd_run -q --network bridge -- installagent "$@"
|
||||
}
|
||||
|
||||
# ── run / shell ─────────────────────────────────────────────────────
|
||||
cmd_run() {
|
||||
require_docker
|
||||
|
||||
local workdir="" container_name="" network="$DEFAULT_NETWORK"
|
||||
local memory="$DEFAULT_MEMORY" cpus="$DEFAULT_CPUS"
|
||||
local read_only=0 mount_home=1 mount_ssh=0 mount_gitconfig=1
|
||||
local rm_flag=1 detach=0 quiet=0 dry_run=0
|
||||
local -a extra_mounts=() env_args=() pass_envs=() env_files=()
|
||||
local -a raw_cmd=()
|
||||
|
||||
# defaults from env/config
|
||||
[[ "${ASANDBOX_MOUNT_SSH:-0}" == "1" ]] && mount_ssh=1
|
||||
[[ "${ASANDBOX_MOUNT_GITCONFIG:-1}" == "0" ]] && mount_gitconfig=0
|
||||
[[ "${ASANDBOX_QUIET:-0}" == "1" ]] && quiet=1
|
||||
|
||||
# Parse ASANDBOX_PASS_ENV
|
||||
if [[ -n "${ASANDBOX_PASS_ENV:-}" ]]; then
|
||||
# shellcheck disable=SC2206
|
||||
pass_envs+=($ASANDBOX_PASS_ENV)
|
||||
fi
|
||||
# shellcheck disable=SC2206
|
||||
pass_envs+=($(default_pass_env_keys))
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-w|--workdir) workdir="${2:-}"; shift 2 ;;
|
||||
-n|--name) container_name="${2:-}"; shift 2 ;;
|
||||
--network) network="${2:-}"; shift 2 ;;
|
||||
--memory) memory="${2:-}"; shift 2 ;;
|
||||
--cpus) cpus="${2:-}"; shift 2 ;;
|
||||
--read-only) read_only=1; shift ;;
|
||||
--no-mount-home) mount_home=0; shift ;;
|
||||
--mount-ssh) mount_ssh=1; shift ;;
|
||||
--mount-gitconfig) mount_gitconfig=1; shift ;;
|
||||
--no-mount-gitconfig) mount_gitconfig=0; shift ;;
|
||||
--mount)
|
||||
extra_mounts+=("${2:-}")
|
||||
shift 2
|
||||
;;
|
||||
--env|-e)
|
||||
env_args+=(--env "${2:-}")
|
||||
shift 2
|
||||
;;
|
||||
--env-file)
|
||||
env_files+=(--env-file "${2:-}")
|
||||
shift 2
|
||||
;;
|
||||
--pass-env)
|
||||
pass_envs+=("${2:-}")
|
||||
shift 2
|
||||
;;
|
||||
--image) IMAGE_NAME="${2:-}"; shift 2 ;;
|
||||
--rm) rm_flag=1; shift ;;
|
||||
--keep) rm_flag=0; shift ;;
|
||||
-d|--detach) detach=1; shift ;;
|
||||
-q|--quiet) quiet=1; shift ;;
|
||||
--dry-run) dry_run=1; shift ;;
|
||||
-h|--help) usage; return 0 ;;
|
||||
--) shift; raw_cmd+=("$@"); break ;;
|
||||
-*)
|
||||
die "unknown option: $1 (try --help)"
|
||||
;;
|
||||
*)
|
||||
raw_cmd+=("$@")
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! image_exists; then
|
||||
warn "image $IMAGE_NAME not found — building now..."
|
||||
cmd_build
|
||||
fi
|
||||
|
||||
local host_wd
|
||||
host_wd=$(resolve_workdir "$workdir")
|
||||
[[ -d "$host_wd" ]] || die "workdir does not exist: $host_wd"
|
||||
|
||||
# Resolve command
|
||||
local -a cmd=()
|
||||
if [[ ${#raw_cmd[@]} -eq 0 ]]; then
|
||||
cmd=(bash)
|
||||
else
|
||||
# read resolve_agent_cmd output into array
|
||||
local line
|
||||
while IFS= read -r line; do
|
||||
cmd+=("$line")
|
||||
done < <(resolve_agent_cmd "${raw_cmd[@]}")
|
||||
fi
|
||||
|
||||
# Unique name
|
||||
if [[ -z "$container_name" ]]; then
|
||||
local base slug
|
||||
base=$(basename "$host_wd" | tr -c 'a-zA-Z0-9._-' '-' | tr '[:upper:]' '[:lower:]')
|
||||
slug=$(echo "${cmd[0]}" | tr -c 'a-zA-Z0-9' '-')
|
||||
container_name="asandbox-${base}-${slug}-$$"
|
||||
# docker name length limit
|
||||
container_name="${container_name:0:60}"
|
||||
fi
|
||||
|
||||
local -a docker_args=(
|
||||
run
|
||||
--name "$container_name"
|
||||
--hostname "asandbox"
|
||||
--network "$network"
|
||||
--memory "$memory"
|
||||
--cpus "$cpus"
|
||||
--security-opt no-new-privileges:true
|
||||
--cap-drop ALL
|
||||
--cap-add CHOWN
|
||||
--cap-add SETUID
|
||||
--cap-add SETGID
|
||||
--cap-add DAC_OVERRIDE
|
||||
--cap-add FOWNER
|
||||
--cap-add NET_BIND_SERVICE
|
||||
# Interactive when stdin is a TTY and not detached
|
||||
)
|
||||
|
||||
if [[ $rm_flag -eq 1 ]]; then
|
||||
docker_args+=(--rm)
|
||||
fi
|
||||
if [[ $detach -eq 1 ]]; then
|
||||
docker_args+=(-d)
|
||||
else
|
||||
docker_args+=(-i)
|
||||
if [[ -t 0 && -t 1 ]]; then
|
||||
docker_args+=(-t)
|
||||
fi
|
||||
fi
|
||||
if [[ $read_only -eq 1 ]]; then
|
||||
docker_args+=(--read-only --tmpfs /tmp:rw,exec,nosuid,size=1g --tmpfs /home/agent:rw,exec,nosuid,size=512m)
|
||||
fi
|
||||
|
||||
# ── filesystem integration ──────────────────────────────────────
|
||||
# 1) Mount workspace at the SAME absolute path (critical for Mac path fidelity)
|
||||
docker_args+=(-v "${host_wd}:${host_wd}:rw")
|
||||
# 2) Also expose as /workspace for convenience
|
||||
docker_args+=(-v "${host_wd}:/workspace:rw")
|
||||
docker_args+=(-w "$host_wd")
|
||||
docker_args+=(-e "ASANDBOX_WORKDIR=$host_wd")
|
||||
docker_args+=(-e "ASANDBOX_HOST_OS=$(uname -s)")
|
||||
docker_args+=(-e "ASANDBOX_QUIET=$quiet")
|
||||
|
||||
# 2b) Persist installagent packages across container runs
|
||||
ensure_persist_dirs
|
||||
docker_args+=(-v "${SHARE_DIR}/persist/.local:/home/agent/.local:rw")
|
||||
docker_args+=(-v "${SHARE_DIR}/persist/.npm-global:/home/agent/.npm-global:rw")
|
||||
docker_args+=(-v "${SHARE_DIR}/persist/.cache:/home/agent/.cache:rw")
|
||||
|
||||
# 3) Agent config / state from host home
|
||||
if [[ $mount_home -eq 1 ]]; then
|
||||
local rel host_path cont_path
|
||||
while IFS= read -r rel; do
|
||||
[[ -z "$rel" ]] && continue
|
||||
host_path="${HOME}/${rel}"
|
||||
cont_path="/home/agent/${rel}"
|
||||
if [[ -e "$host_path" ]]; then
|
||||
# ensure parent exists in container via mount of the path itself
|
||||
docker_args+=(-v "${host_path}:${cont_path}:rw")
|
||||
fi
|
||||
done < <(agent_home_mounts)
|
||||
fi
|
||||
|
||||
# 4) SSH keys (optional, read-only)
|
||||
if [[ $mount_ssh -eq 1 && -d "${HOME}/.ssh" ]]; then
|
||||
docker_args+=(-v "${HOME}/.ssh:/home/agent/.ssh:ro")
|
||||
fi
|
||||
|
||||
# 5) gitconfig
|
||||
if [[ $mount_gitconfig -eq 1 && -f "${HOME}/.gitconfig" ]]; then
|
||||
docker_args+=(-v "${HOME}/.gitconfig:/home/agent/.gitconfig:ro")
|
||||
fi
|
||||
|
||||
# 6) Extra mounts
|
||||
local m host_m cont_m
|
||||
for m in "${extra_mounts[@]+"${extra_mounts[@]}"}"; do
|
||||
if [[ "$m" == *:* ]]; then
|
||||
host_m="${m%%:*}"
|
||||
cont_m="${m#*:}"
|
||||
else
|
||||
host_m="$m"
|
||||
cont_m="$m"
|
||||
fi
|
||||
# resolve host side if relative
|
||||
if [[ "$host_m" != /* ]]; then
|
||||
host_m="$(cd "$(dirname "$host_m")" && pwd -P)/$(basename "$host_m")"
|
||||
fi
|
||||
docker_args+=(-v "${host_m}:${cont_m}")
|
||||
done
|
||||
|
||||
# 7) Pass through env (bash 3.2-compatible dedupe)
|
||||
local key seen_env=" "
|
||||
for key in "${pass_envs[@]+"${pass_envs[@]}"}"; do
|
||||
[[ -z "$key" ]] && continue
|
||||
case "$seen_env" in
|
||||
*" ${key} "*) continue ;;
|
||||
esac
|
||||
seen_env+="${key} "
|
||||
# indirect expansion works in bash 3.2
|
||||
if [[ -n "${!key:-}" ]]; then
|
||||
docker_args+=(-e "${key}=${!key}")
|
||||
fi
|
||||
done
|
||||
|
||||
# Git author from host
|
||||
local gname gemail
|
||||
gname=$(git config --global user.name 2>/dev/null || true)
|
||||
gemail=$(git config --global user.email 2>/dev/null || true)
|
||||
[[ -n "$gname" ]] && docker_args+=(-e "GIT_AUTHOR_NAME=$gname" -e "GIT_COMMITTER_NAME=$gname")
|
||||
[[ -n "$gemail" ]] && docker_args+=(-e "GIT_AUTHOR_EMAIL=$gemail" -e "GIT_COMMITTER_EMAIL=$gemail")
|
||||
|
||||
docker_args+=("${env_args[@]+"${env_args[@]}"}")
|
||||
docker_args+=("${env_files[@]+"${env_files[@]}"}")
|
||||
|
||||
# Label for cleanup
|
||||
docker_args+=(--label "asandbox=1" --label "asandbox.workdir=${host_wd}")
|
||||
|
||||
docker_args+=("$IMAGE_NAME")
|
||||
docker_args+=("${cmd[@]}")
|
||||
|
||||
if [[ $quiet -eq 0 ]]; then
|
||||
info "workdir ${C_DIM}${host_wd}${C_RST}"
|
||||
info "network ${C_DIM}${network}${C_RST} memory=${memory} cpus=${cpus}"
|
||||
info "command ${C_DIM}${cmd[*]}${C_RST}"
|
||||
fi
|
||||
|
||||
if [[ $dry_run -eq 1 ]]; then
|
||||
echo "docker $(printf '%q ' "${docker_args[@]}")"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Never mount docker.sock — explicit safety check
|
||||
local a
|
||||
for a in "${docker_args[@]}"; do
|
||||
if [[ "$a" == *docker.sock* ]]; then
|
||||
die "refusing to mount docker.sock"
|
||||
fi
|
||||
done
|
||||
|
||||
exec docker "${docker_args[@]}"
|
||||
}
|
||||
|
||||
cmd_shell() {
|
||||
cmd_run "$@" -- bash
|
||||
}
|
||||
|
||||
cmd_config() {
|
||||
case "${1:-show}" in
|
||||
show|cat)
|
||||
if [[ -f "$CONFIG_FILE" ]]; then
|
||||
echo "# $CONFIG_FILE"
|
||||
cat "$CONFIG_FILE"
|
||||
else
|
||||
warn "no config yet — run: asandbox config init"
|
||||
fi
|
||||
;;
|
||||
init|create)
|
||||
write_default_config
|
||||
;;
|
||||
path)
|
||||
echo "$CONFIG_FILE"
|
||||
;;
|
||||
*)
|
||||
die "usage: asandbox config [show|init|path]"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ── main ────────────────────────────────────────────────────────────
|
||||
main() {
|
||||
load_config
|
||||
|
||||
local cmd="${1:-}"
|
||||
if [[ -z "$cmd" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
shift || true
|
||||
|
||||
case "$cmd" in
|
||||
run) cmd_run "$@" ;;
|
||||
shell|sh|bash) cmd_shell "$@" ;;
|
||||
installagent|install-agent|ia)
|
||||
cmd_installagent "$@"
|
||||
;;
|
||||
build) cmd_build "$@" ;;
|
||||
agents|list) cmd_agents "$@" ;;
|
||||
config) cmd_config "$@" ;;
|
||||
doctor) cmd_doctor "$@" ;;
|
||||
version|--version|-V) echo "asandbox $VERSION" ;;
|
||||
help|-h|--help) usage ;;
|
||||
# Convenience: asandbox claude ... → asandbox run claude ...
|
||||
claude|claude-code|codex|gemini|gemini-cli|aider|antigravity|agy|cursor|opencode|qwen|amp|goose)
|
||||
cmd_run "$cmd" "$@"
|
||||
;;
|
||||
*)
|
||||
# Unknown subcommand: treat as agent/command name
|
||||
cmd_run "$cmd" "$@"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,17 @@
|
||||
# Sourced for interactive shells inside asandbox
|
||||
export PATH="${HOME}/.local/bin:${HOME}/.npm-global/bin:${PATH}"
|
||||
|
||||
# npm user prefix for installagent
|
||||
if command -v npm >/dev/null 2>&1; then
|
||||
npm config set prefix "${HOME}/.npm-global" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
alias install-agent='installagent'
|
||||
alias agents='installagent list'
|
||||
|
||||
# Helpful tip once per shell when installagent exists
|
||||
if [[ $- == *i* ]] && command -v installagent >/dev/null 2>&1; then
|
||||
if [[ "${ASANDBOX_QUIET:-0}" != "1" && "${ASANDBOX_NO_TIP:-0}" != "1" ]]; then
|
||||
echo "tip: installagent list | installagent claude|cursor|opencode|antigravity|gemini|..." >&2
|
||||
fi
|
||||
fi
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
# asandbox container entrypoint — normalize env, ensure dirs, run command
|
||||
set -euo pipefail
|
||||
|
||||
# Ensure common agent config + install dirs exist (host mounts may be empty)
|
||||
mkdir -p \
|
||||
"${HOME}/.claude" \
|
||||
"${HOME}/.codex" \
|
||||
"${HOME}/.config" \
|
||||
"${HOME}/.local/bin" \
|
||||
"${HOME}/.npm-global/bin" \
|
||||
"${HOME}/.ssh" \
|
||||
"${HOME}/.npm" \
|
||||
"${HOME}/.gemini" \
|
||||
"${HOME}/.cursor" \
|
||||
2>/dev/null || true
|
||||
|
||||
export PATH="${HOME}/.local/bin:${HOME}/.npm-global/bin:/usr/local/bin:${PATH}"
|
||||
|
||||
# npm prefix for user installs via installagent
|
||||
if command -v npm >/dev/null 2>&1; then
|
||||
npm config set prefix "${HOME}/.npm-global" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Git identity fallback so commits work without host gitconfig mount
|
||||
if [[ -n "${GIT_AUTHOR_NAME:-}" ]]; then
|
||||
git config --global user.name "${GIT_AUTHOR_NAME}" 2>/dev/null || true
|
||||
fi
|
||||
if [[ -n "${GIT_AUTHOR_EMAIL:-}" ]]; then
|
||||
git config --global user.email "${GIT_AUTHOR_EMAIL}" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Respect ASANDBOX_WORKDIR if set
|
||||
if [[ -n "${ASANDBOX_WORKDIR:-}" && -d "${ASANDBOX_WORKDIR}" ]]; then
|
||||
cd "${ASANDBOX_WORKDIR}"
|
||||
fi
|
||||
|
||||
# Interactive shell banner
|
||||
_is_shell=0
|
||||
if [[ $# -eq 0 || "${1:-}" == "bash" || "${1:-}" == "sh" || "${1:-}" == "bash" ]]; then
|
||||
_is_shell=1
|
||||
fi
|
||||
|
||||
if [[ "${ASANDBOX_BANNER:-0}" == "1" ]] || { [[ -t 0 ]] && [[ $_is_shell -eq 1 ]]; }; then
|
||||
if [[ "${ASANDBOX_QUIET:-0}" != "1" ]]; then
|
||||
echo "┌──────────────────────────────────────────────────┐" >&2
|
||||
echo "│ asandbox — isolated agent environment │" >&2
|
||||
echo "│ cwd: $(pwd)" >&2
|
||||
echo "│ │" >&2
|
||||
echo "│ installagent list │" >&2
|
||||
echo "│ installagent <name> # claude cursor opencode │" >&2
|
||||
echo "│ # antigravity gemini … │" >&2
|
||||
echo "└──────────────────────────────────────────────────┘" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
exec bash -l
|
||||
fi
|
||||
|
||||
# If launching bash login/interactive, keep as-is
|
||||
exec "$@"
|
||||
+409
@@ -0,0 +1,409 @@
|
||||
#!/usr/bin/env bash
|
||||
# installagent — install CLI AI coding agents inside asandbox
|
||||
# Usage: installagent <name> | installagent list | installagent status
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="1.1.0"
|
||||
|
||||
if [[ -t 2 ]]; then
|
||||
C_RED=$'\033[31m'; C_GRN=$'\033[32m'; C_YLW=$'\033[33m'
|
||||
C_BLU=$'\033[34m'; C_DIM=$'\033[2m'; C_RST=$'\033[0m'; C_BLD=$'\033[1m'
|
||||
else
|
||||
C_RED=; C_GRN=; C_YLW=; C_BLU=; C_DIM=; C_RST=; C_BLD=
|
||||
fi
|
||||
|
||||
die() { echo "${C_RED}installagent:${C_RST} $*" >&2; exit 1; }
|
||||
info() { echo "${C_BLU}installagent:${C_RST} $*" >&2; }
|
||||
ok() { echo "${C_GRN}installagent:${C_RST} $*" >&2; }
|
||||
warn() { echo "${C_YLW}installagent:${C_RST} $*" >&2; }
|
||||
|
||||
ensure_local_path() {
|
||||
mkdir -p "${HOME}/.local/bin" "${HOME}/.local/lib" "${HOME}/.npm-global"
|
||||
# Prefer user-local npm so installs survive container recreate (via persist mount)
|
||||
if command -v npm >/dev/null 2>&1; then
|
||||
npm config set prefix "${HOME}/.npm-global" >/dev/null 2>&1 || true
|
||||
fi
|
||||
export PATH="${HOME}/.local/bin:${HOME}/.npm-global/bin:${PATH}"
|
||||
}
|
||||
|
||||
npm_g() {
|
||||
ensure_local_path
|
||||
npm install -g --prefix "${HOME}/.npm-global" "$@"
|
||||
}
|
||||
|
||||
have_cmd() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
# ── catalog ─────────────────────────────────────────────────────────
|
||||
# id|aliases|binary|description
|
||||
catalog() {
|
||||
cat <<'EOF'
|
||||
antigravity|agy,antigravity-cli|agy|Google Antigravity CLI
|
||||
cursor|cursor-cli,cursor-agent|agent|Cursor Agent CLI
|
||||
opencode|open-code|opencode|OpenCode open-source coding agent
|
||||
claude|claude-code,claude-cli|claude|Anthropic Claude Code
|
||||
gemini|gemini-cli,google-gemini|gemini|Google Gemini CLI
|
||||
codex|openai-codex|codex|OpenAI Codex CLI
|
||||
aider|aider-chat|aider|Aider Python coding agent
|
||||
qwen|qwen-code,qwen-cli|qwen|Qwen Code CLI
|
||||
amp|sourcegraph-amp|amp|Sourcegraph Amp CLI
|
||||
goose|block-goose|goose|Block Goose agent
|
||||
continue|cn|cn|Continue CLI
|
||||
EOF
|
||||
}
|
||||
|
||||
normalize_name() {
|
||||
echo "$1" | tr '[:upper:]' '[:lower:]' | tr '_' '-'
|
||||
}
|
||||
|
||||
resolve_id() {
|
||||
local want
|
||||
want=$(normalize_name "$1")
|
||||
local line id aliases
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" || "$line" == \#* ]] && continue
|
||||
id="${line%%|*}"
|
||||
aliases="${line#*|}"
|
||||
aliases="${aliases%%|*}"
|
||||
if [[ "$want" == "$id" ]]; then
|
||||
echo "$id"
|
||||
return 0
|
||||
fi
|
||||
local a
|
||||
IFS=',' read -ra _als <<< "$aliases"
|
||||
for a in "${_als[@]}"; do
|
||||
a=$(echo "$a" | tr -d ' ')
|
||||
if [[ "$want" == "$a" ]]; then
|
||||
echo "$id"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
done < <(catalog)
|
||||
return 1
|
||||
}
|
||||
|
||||
agent_binary() {
|
||||
local id="$1" line
|
||||
while IFS= read -r line; do
|
||||
[[ "${line%%|*}" == "$id" ]] || continue
|
||||
# id|aliases|binary|desc
|
||||
local rest="${line#*|}"
|
||||
rest="${rest#*|}"
|
||||
echo "${rest%%|*}"
|
||||
return 0
|
||||
done < <(catalog)
|
||||
}
|
||||
|
||||
agent_desc() {
|
||||
local id="$1" line
|
||||
while IFS= read -r line; do
|
||||
[[ "${line%%|*}" == "$id" ]] || continue
|
||||
echo "${line##*|}"
|
||||
return 0
|
||||
done < <(catalog)
|
||||
}
|
||||
|
||||
# ── installers ──────────────────────────────────────────────────────
|
||||
install_antigravity() {
|
||||
info "installing Antigravity CLI (agy)..."
|
||||
ensure_local_path
|
||||
curl -fsSL https://antigravity.google/cli/install.sh | bash
|
||||
# Some installers put binary only in a custom dir — ensure on PATH
|
||||
if ! have_cmd agy; then
|
||||
# common locations
|
||||
for p in \
|
||||
"${HOME}/.local/bin/agy" \
|
||||
"${HOME}/.antigravity/bin/agy" \
|
||||
"${HOME}/.agy/bin/agy" \
|
||||
/usr/local/bin/agy; do
|
||||
if [[ -x "$p" ]]; then
|
||||
mkdir -p "${HOME}/.local/bin"
|
||||
ln -sfn "$p" "${HOME}/.local/bin/agy"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
have_cmd agy || die "agy not found after install — check installer output"
|
||||
ok "Antigravity CLI ready: $(command -v agy)"
|
||||
agy --version 2>/dev/null || agy version 2>/dev/null || true
|
||||
}
|
||||
|
||||
install_cursor() {
|
||||
info "installing Cursor Agent CLI..."
|
||||
ensure_local_path
|
||||
curl -fsSL https://cursor.com/install | bash
|
||||
if ! have_cmd agent; then
|
||||
for p in \
|
||||
"${HOME}/.local/bin/agent" \
|
||||
"${HOME}/.cursor/bin/agent" \
|
||||
"${HOME}/.cursor-agent/bin/agent"; do
|
||||
if [[ -x "$p" ]]; then
|
||||
mkdir -p "${HOME}/.local/bin"
|
||||
ln -sfn "$p" "${HOME}/.local/bin/agent"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
have_cmd agent || die "agent not found after install"
|
||||
ok "Cursor CLI ready: $(command -v agent)"
|
||||
agent --version 2>/dev/null || true
|
||||
}
|
||||
|
||||
install_opencode() {
|
||||
info "installing OpenCode..."
|
||||
ensure_local_path
|
||||
if have_cmd npm; then
|
||||
npm_g opencode-ai
|
||||
else
|
||||
curl -fsSL https://opencode.ai/install | bash
|
||||
fi
|
||||
if ! have_cmd opencode; then
|
||||
for p in "${HOME}/.npm-global/bin/opencode" "${HOME}/.local/bin/opencode" /usr/local/bin/opencode; do
|
||||
[[ -x "$p" ]] && ln -sfn "$p" "${HOME}/.local/bin/opencode" && break
|
||||
done
|
||||
fi
|
||||
have_cmd opencode || die "opencode not found after install"
|
||||
ok "OpenCode ready: $(command -v opencode)"
|
||||
opencode --version 2>/dev/null || true
|
||||
}
|
||||
|
||||
install_claude() {
|
||||
info "installing Claude Code..."
|
||||
have_cmd npm || die "npm required (Node.js)"
|
||||
npm_g @anthropic-ai/claude-code
|
||||
have_cmd claude || die "claude not found after install"
|
||||
ok "Claude Code ready: $(command -v claude)"
|
||||
claude --version 2>/dev/null || true
|
||||
}
|
||||
|
||||
install_gemini() {
|
||||
info "installing Gemini CLI..."
|
||||
have_cmd npm || die "npm required (Node.js)"
|
||||
npm_g @google/gemini-cli
|
||||
have_cmd gemini || die "gemini not found after install"
|
||||
ok "Gemini CLI ready: $(command -v gemini)"
|
||||
gemini --version 2>/dev/null || true
|
||||
}
|
||||
|
||||
install_codex() {
|
||||
info "installing OpenAI Codex CLI..."
|
||||
have_cmd npm || die "npm required (Node.js)"
|
||||
npm_g @openai/codex
|
||||
have_cmd codex || die "codex not found after install"
|
||||
ok "Codex ready: $(command -v codex)"
|
||||
codex --version 2>/dev/null || true
|
||||
}
|
||||
|
||||
install_aider() {
|
||||
info "installing Aider..."
|
||||
ensure_local_path
|
||||
if have_cmd uv; then
|
||||
uv tool install aider-chat
|
||||
elif have_cmd pip3; then
|
||||
pip3 install --user --break-system-packages aider-chat \
|
||||
|| pip3 install --user aider-chat
|
||||
else
|
||||
die "need uv or pip3 to install aider"
|
||||
fi
|
||||
# uv tools go to ~/.local/bin
|
||||
have_cmd aider || die "aider not found after install — ensure ~/.local/bin is on PATH"
|
||||
ok "Aider ready: $(command -v aider)"
|
||||
aider --version 2>/dev/null || true
|
||||
}
|
||||
|
||||
install_qwen() {
|
||||
info "installing Qwen Code..."
|
||||
have_cmd npm || die "npm required (Node.js)"
|
||||
npm_g @qwen-code/qwen-code
|
||||
# binary name may be qwen or qwen-code
|
||||
if ! have_cmd qwen && have_cmd qwen-code; then
|
||||
ln -sfn "$(command -v qwen-code)" "${HOME}/.local/bin/qwen"
|
||||
fi
|
||||
if have_cmd qwen || have_cmd qwen-code; then
|
||||
ok "Qwen Code ready: $(command -v qwen 2>/dev/null || command -v qwen-code)"
|
||||
else
|
||||
die "qwen binary not found after install"
|
||||
fi
|
||||
}
|
||||
|
||||
install_amp() {
|
||||
info "installing Amp (Sourcegraph)..."
|
||||
ensure_local_path
|
||||
# Official installer when available
|
||||
if curl -fsSL https://ampcode.com/install.sh 2>/dev/null | bash; then
|
||||
:
|
||||
elif have_cmd npm; then
|
||||
npm_g @sourcegraph/amp 2>/dev/null || npm_g amp 2>/dev/null || true
|
||||
fi
|
||||
have_cmd amp || warn "amp may need manual install — see https://ampcode.com"
|
||||
have_cmd amp && ok "Amp ready: $(command -v amp)"
|
||||
}
|
||||
|
||||
install_goose() {
|
||||
info "installing Goose..."
|
||||
ensure_local_path
|
||||
curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh 2>/dev/null | bash \
|
||||
|| curl -fsSL https://raw.githubusercontent.com/block/goose/main/download_cli.sh 2>/dev/null | bash \
|
||||
|| true
|
||||
if ! have_cmd goose; then
|
||||
# fallback cargo if present
|
||||
if have_cmd cargo; then
|
||||
cargo install goose-cli 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
have_cmd goose || die "goose not found after install — see https://github.com/block/goose"
|
||||
ok "Goose ready: $(command -v goose)"
|
||||
}
|
||||
|
||||
install_continue() {
|
||||
info "installing Continue CLI..."
|
||||
ensure_local_path
|
||||
have_cmd npm || die "npm required"
|
||||
npm_g @continuedev/cli 2>/dev/null || npm_g continue-cli 2>/dev/null || true
|
||||
if have_cmd cn || have_cmd continue; then
|
||||
ok "Continue ready: $(command -v cn 2>/dev/null || command -v continue)"
|
||||
else
|
||||
warn "Continue CLI package name may have changed — try: npm install -g @continuedev/cli"
|
||||
fi
|
||||
}
|
||||
|
||||
do_install() {
|
||||
local id="$1"
|
||||
case "$id" in
|
||||
antigravity) install_antigravity ;;
|
||||
cursor) install_cursor ;;
|
||||
opencode) install_opencode ;;
|
||||
claude) install_claude ;;
|
||||
gemini) install_gemini ;;
|
||||
codex) install_codex ;;
|
||||
aider) install_aider ;;
|
||||
qwen) install_qwen ;;
|
||||
amp) install_amp ;;
|
||||
goose) install_goose ;;
|
||||
continue) install_continue ;;
|
||||
*) die "no installer for: $id" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_list() {
|
||||
echo "${C_BLD}Available agents${C_RST} (installagent <name>)"
|
||||
echo
|
||||
printf " ${C_BLD}%-14s${C_RST} %-12s %s\n" "NAME" "BINARY" "DESCRIPTION"
|
||||
printf " %-14s %-12s %s\n" "----" "------" "-----------"
|
||||
local line id aliases bin desc
|
||||
while IFS= read -r line; do
|
||||
id="${line%%|*}"
|
||||
rest="${line#*|}"
|
||||
aliases="${rest%%|*}"
|
||||
rest="${rest#*|}"
|
||||
bin="${rest%%|*}"
|
||||
desc="${rest#*|}"
|
||||
local mark=" "
|
||||
if have_cmd "$bin"; then
|
||||
mark="${C_GRN}✓${C_RST}"
|
||||
else
|
||||
mark="${C_DIM}·${C_RST}"
|
||||
fi
|
||||
printf " %s %-12s %-12s %s\n" "$mark" "$id" "$bin" "$desc"
|
||||
if [[ -n "$aliases" ]]; then
|
||||
printf " ${C_DIM}aliases: %s${C_RST}\n" "$aliases"
|
||||
fi
|
||||
done < <(catalog)
|
||||
echo
|
||||
echo "Examples:"
|
||||
echo " installagent claude"
|
||||
echo " installagent antigravity"
|
||||
echo " installagent cursor"
|
||||
echo " installagent opencode"
|
||||
echo " installagent all # install core set"
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
ensure_local_path
|
||||
echo "${C_BLD}Installed agents${C_RST}"
|
||||
local line id bin
|
||||
while IFS= read -r line; do
|
||||
id="${line%%|*}"
|
||||
rest="${line#*|}"
|
||||
rest="${rest#*|}"
|
||||
bin="${rest%%|*}"
|
||||
if have_cmd "$bin"; then
|
||||
printf " ${C_GRN}✓${C_RST} %-14s %s\n" "$id" "$(command -v "$bin")"
|
||||
else
|
||||
printf " ${C_DIM}·${C_RST} %-14s (not installed)\n" "$id"
|
||||
fi
|
||||
done < <(catalog)
|
||||
echo
|
||||
echo "PATH includes:"
|
||||
echo " ${HOME}/.local/bin"
|
||||
echo " ${HOME}/.npm-global/bin"
|
||||
}
|
||||
|
||||
cmd_all() {
|
||||
# Core set commonly used
|
||||
local core=(claude gemini codex opencode cursor antigravity aider)
|
||||
local name
|
||||
for name in "${core[@]}"; do
|
||||
info "── $name ──"
|
||||
if do_install "$name"; then
|
||||
ok "$name done"
|
||||
else
|
||||
warn "$name failed (continuing)"
|
||||
fi
|
||||
done
|
||||
cmd_status
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
installagent v${VERSION} — install CLI AI agents inside asandbox
|
||||
|
||||
USAGE
|
||||
installagent <name> Install agent by name or alias
|
||||
installagent list Show catalog
|
||||
installagent status Show what is installed
|
||||
installagent all Install core agents
|
||||
installagent help This help
|
||||
|
||||
AGENTS
|
||||
antigravity (agy) Google Antigravity CLI
|
||||
cursor (agent) Cursor Agent CLI
|
||||
opencode OpenCode
|
||||
claude Claude Code
|
||||
gemini Gemini CLI
|
||||
codex OpenAI Codex
|
||||
aider Aider
|
||||
qwen Qwen Code
|
||||
amp Sourcegraph Amp
|
||||
goose Block Goose
|
||||
continue Continue CLI
|
||||
|
||||
Installs go to ~/.local and ~/.npm-global (persisted by asandbox).
|
||||
EOF
|
||||
}
|
||||
|
||||
main() {
|
||||
ensure_local_path
|
||||
|
||||
local cmd="${1:-list}"
|
||||
shift || true
|
||||
|
||||
case "$cmd" in
|
||||
list|ls|-l) cmd_list ;;
|
||||
status|st) cmd_status ;;
|
||||
all) cmd_all ;;
|
||||
help|-h|--help) usage ;;
|
||||
version|-V|--version) echo "installagent $VERSION" ;;
|
||||
*)
|
||||
local id
|
||||
if ! id=$(resolve_id "$cmd"); then
|
||||
die "unknown agent: $cmd (try: installagent list)"
|
||||
fi
|
||||
info "resolved '$cmd' → $id ($(agent_desc "$id"))"
|
||||
do_install "$id"
|
||||
echo
|
||||
ok "run: $(agent_binary "$id")"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user