# opencode-devbox — portable AI dev environment
# Debian-based container with opencode and configurable dev tools

ARG DEBIAN_VERSION=trixie-slim
FROM debian:${DEBIAN_VERSION} AS base

ARG TARGETARCH
ARG OPENCODE_VERSION=1.14.29

LABEL maintainer="joakimp"
LABEL description="Portable opencode developer container"
LABEL org.opencontainers.image.source="https://gitea.jordbo.se/joakimp/opencode-devbox"

# Avoid interactive prompts during build
ENV DEBIAN_FRONTEND=noninteractive

# ── Core system packages ─────────────────────────────────────────────
# apt-get upgrade picks up any security/CVE fixes published between
# debian:trixie-slim base-image rebuilds. Paired with the index update
# and the install in the same layer so we don't bloat image history.
RUN apt-get update && \
    apt-get upgrade -y --no-install-recommends && \
    apt-get install -y --no-install-recommends \
    ca-certificates \
    curl \
    wget \
    git \
    openssh-client \
    gnupg \
    jq \
    ripgrep \
    fd-find \
    tree \
    less \
    htop \
    tmux \
    make \
    patch \
    diffutils \
    git-crypt \
    age \
    file \
    sudo \
    locales \
    procps \
    unzip \
    gcc \
    g++ \
    rsync \
    python3-pip \
    python3-venv \
    && ln -s /usr/bin/fdfind /usr/local/bin/fd \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# ── Go-compiled tools (install from GitHub to avoid CVEs in Debian's old Go builds)
#
# Version policy for the binaries below:
#   • Default is `latest` — resolved at build time by following the
#     /releases/latest redirect on GitHub and reading the tag from the
#     Location header. This means every tagged image picks up the newest
#     upstream release, with no risk of running months-old CVE-affected
#     binaries.
#   • Explicit pins still work: pass `--build-arg GOSU_VERSION=1.19` etc.
#     Useful for reproducibility or rolling back a bad upstream release.
#   • Resolved versions are printed during build and re-checked by the
#     smoke test (scripts/smoke-test.sh), so drift is visible in CI logs.
#
# The helper `resolve_latest` reads the redirected tag (e.g. "v0.26.1")
# and strips a leading "v" if present, yielding a plain version string.

# gosu — privilege de-escalation
ARG GOSU_VERSION=latest
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
    V="${GOSU_VERSION}" && \
    if [ "$V" = "latest" ]; then \
      V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/tianon/gosu/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
    fi && \
    V="${V#v}" && \
    [ -n "$V" ] && \
    echo "Installing gosu ${V}" && \
    curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/tianon/gosu/releases/download/${V}/gosu-${ARCH}" -o /usr/local/bin/gosu && \
    chmod +x /usr/local/bin/gosu && \
    gosu --version

# fzf — fuzzy finder
ARG FZF_VERSION=latest
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
    V="${FZF_VERSION}" && \
    if [ "$V" = "latest" ]; then \
      V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/junegunn/fzf/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
    fi && \
    V="${V#v}" && \
    [ -n "$V" ] && \
    echo "Installing fzf ${V}" && \
    curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/junegunn/fzf/releases/download/v${V}/fzf-${V}-linux_${ARCH}.tar.gz" | tar -xz -C /usr/local/bin fzf && \
    fzf --version

# git-lfs — Git Large File Storage
ARG GIT_LFS_VERSION=latest
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
    V="${GIT_LFS_VERSION}" && \
    if [ "$V" = "latest" ]; then \
      V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/git-lfs/git-lfs/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
    fi && \
    V="${V#v}" && \
    [ -n "$V" ] && \
    echo "Installing git-lfs ${V}" && \
    curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/git-lfs/git-lfs/releases/download/v${V}/git-lfs-linux-${ARCH}-v${V}.tar.gz" | tar -xz -C /tmp && \
    install /tmp/git-lfs-${V}/git-lfs /usr/local/bin/git-lfs && \
    rm -rf /tmp/git-lfs-${V} && \
    git lfs install --system && \
    git-lfs --version

# neovim — modern text editor
ARG NVIM_VERSION=latest
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "arm64" ;; *) echo "x86_64" ;; esac) && \
    V="${NVIM_VERSION}" && \
    if [ "$V" = "latest" ]; then \
      V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/neovim/neovim/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
    fi && \
    V="${V#v}" && \
    [ -n "$V" ] && \
    echo "Installing neovim ${V}" && \
    curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/neovim/neovim/releases/download/v${V}/nvim-linux-${ARCH}.tar.gz" | tar -xz -C /opt && \
    ln -s /opt/nvim-linux-${ARCH}/bin/nvim /usr/local/bin/nvim && \
    nvim --version | head -1

# bat — syntax-highlighted cat replacement
ARG BAT_VERSION=latest
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
    V="${BAT_VERSION}" && \
    if [ "$V" = "latest" ]; then \
      V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/sharkdp/bat/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
    fi && \
    V="${V#v}" && \
    [ -n "$V" ] && \
    echo "Installing bat ${V}" && \
    curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/sharkdp/bat/releases/download/v${V}/bat-v${V}-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /tmp && \
    install /tmp/bat-v${V}-${ARCH}-unknown-linux-musl/bat /usr/local/bin/bat && \
    rm -rf /tmp/bat-v${V}-* && \
    bat --version

# eza — modern ls replacement
ARG EZA_VERSION=latest
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
    V="${EZA_VERSION}" && \
    if [ "$V" = "latest" ]; then \
      V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/eza-community/eza/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
    fi && \
    V="${V#v}" && \
    [ -n "$V" ] && \
    echo "Installing eza ${V}" && \
    curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/eza-community/eza/releases/download/v${V}/eza_${ARCH}-unknown-linux-gnu.tar.gz" | tar -xz -C /usr/local/bin && \
    eza --version | head -1

# zoxide — smarter cd command
ARG ZOXIDE_VERSION=latest
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
    V="${ZOXIDE_VERSION}" && \
    if [ "$V" = "latest" ]; then \
      V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/ajeetdsouza/zoxide/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
    fi && \
    V="${V#v}" && \
    [ -n "$V" ] && \
    echo "Installing zoxide ${V}" && \
    curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/ajeetdsouza/zoxide/releases/download/v${V}/zoxide-${V}-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /usr/local/bin zoxide && \
    zoxide --version

# uv — fast Python package manager (replaces pip, venv, pyenv)
# Note: uv releases don't prefix tags with "v" (e.g. tag is "0.11.8").
ARG UV_VERSION=latest
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
    V="${UV_VERSION}" && \
    if [ "$V" = "latest" ]; then \
      V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/astral-sh/uv/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
    fi && \
    V="${V#v}" && \
    [ -n "$V" ] && \
    echo "Installing uv ${V}" && \
    curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/astral-sh/uv/releases/download/${V}/uv-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /tmp && \
    install /tmp/uv-${ARCH}-unknown-linux-musl/uv /usr/local/bin/uv && \
    install /tmp/uv-${ARCH}-unknown-linux-musl/uvx /usr/local/bin/uvx && \
    rm -rf /tmp/uv-* && \
    uv --version

# ── Optional: MemPalace — local-first AI memory system ───────────────
# Provides semantic search over conversation history via 29 MCP tools.
# Palace data persists via the devbox-palace named volume.
# The embedding model (~300 MB) is downloaded on first use and cached
# in the palace directory.
#
# Installed via `uv tool install` into an isolated venv at
# /opt/uv-tools/mempalace/. The `mempalace` CLI goes directly on PATH;
# the MCP server is reached via the /usr/local/bin/mempalace-mcp-server
# wrapper (rootfs/usr/local/bin/mempalace-mcp-server), since system
# python3 cannot import from the isolated venv.
#
# Disable with --build-arg INSTALL_MEMPALACE=false to shave ~300 MB off
# the image (chromadb, torch-adjacent deps).
ARG INSTALL_MEMPALACE=true
ENV UV_TOOL_DIR=/opt/uv-tools
ENV UV_TOOL_BIN_DIR=/usr/local/bin
RUN if [ "${INSTALL_MEMPALACE}" = "true" ]; then \
      mkdir -p /opt/uv-tools && \
      uv tool install --no-cache mempalace && \
      /opt/uv-tools/mempalace/bin/python -c "import mempalace; print('mempalace', mempalace.__version__ if hasattr(mempalace, '__version__') else 'installed')" ; \
    fi

# rustup — Rust toolchain manager
# Installs the rustup-init binary only. Users bootstrap Rust with:
#   rustup-init -y && source ~/.cargo/env
# Toolchains persist via devbox-rustup and devbox-cargo volumes.
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
    curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://static.rust-lang.org/rustup/dist/${ARCH}-unknown-linux-gnu/rustup-init" -o /usr/local/bin/rustup-init && \
    chmod +x /usr/local/bin/rustup-init

# gitea-mcp — MCP server for Gitea API (official, Go binary, hosted on gitea.com)
ARG GITEA_MCP_VERSION=latest
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "arm64" ;; *) echo "x86_64" ;; esac) && \
    V="${GITEA_MCP_VERSION}" && \
    if [ "$V" = "latest" ]; then \
      V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://gitea.com/gitea/gitea-mcp/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
    fi && \
    V="${V#v}" && \
    [ -n "$V" ] && \
    echo "Installing gitea-mcp ${V}" && \
    curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://gitea.com/gitea/gitea-mcp/releases/download/v${V}/gitea-mcp_Linux_${ARCH}.tar.gz" \
    | tar -xz -C /usr/local/bin/ gitea-mcp && \
    chmod +x /usr/local/bin/gitea-mcp && \
    gitea-mcp --version

# Set locale — generate common UTF-8 locales (override via LANG/LC_ALL env vars)
# To add more locales, run: sudo sed -i '/<locale>.UTF-8/s/^# //g' /etc/locale.gen && sudo locale-gen
RUN sed -i -E '/(en_US|en_GB|sv_SE|da_DK|nb_NO|fi_FI|de_DE|fr_FR|es_ES|it_IT|pt_BR|nl_NL|pl_PL|ja_JP|ko_KR|zh_CN)\.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
ENV LANG=en_US.UTF-8
ENV LANGUAGE=en_US:en
ENV LC_ALL=en_US.UTF-8
ENV EDITOR=nvim
ENV PATH="/home/developer/.local/bin:/home/developer/.cargo/bin:${PATH}"

# ── Node.js (required for opencode v1.x install + MCP servers) ──────
ARG NODE_VERSION=22
RUN curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - && \
    apt-get install -y --no-install-recommends nodejs && \
    rm -rf /var/lib/apt/lists/*

# ── Install opencode via npm ─────────────────────────────────────────
# v1.x is distributed as an npm package with platform-specific binaries
RUN npm install -g opencode-ai@${OPENCODE_VERSION} && \
    opencode --version

# ── AWS CLI v2 (for SSO/Bedrock authentication) ─────────────────────
RUN ARCH=$(case "${TARGETARCH}" in \
      amd64) echo "x86_64" ;; \
      arm64) echo "aarch64" ;; \
      *) echo "x86_64" ;; \
    esac) && \
    curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://awscli.amazonaws.com/awscli-exe-linux-${ARCH}.zip" -o /tmp/awscli.zip && \
    unzip -q /tmp/awscli.zip -d /tmp && \
    /tmp/aws/install && \
    rm -rf /tmp/aws /tmp/awscli.zip && \
     aws --version

# ── Optional: Go ─────────────────────────────────────────────────────
# Latest stable Go is resolved from https://go.dev/dl/?mode=json when
# GO_VERSION=latest (default). Pass an explicit version like "1.26.2"
# to pin.
ARG INSTALL_GO=false
ARG GO_VERSION=latest
RUN if [ "${INSTALL_GO}" = "true" ]; then \
      GOARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
      V="${GO_VERSION}" && \
      if [ "$V" = "latest" ]; then \
        V=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://go.dev/dl/?mode=json" | \
            awk -F'"' '/"version":/ { sub(/^go/,"",$4); print $4; exit }'); \
      fi && \
      [ -n "$V" ] && \
      echo "Installing Go ${V}" && \
      curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://go.dev/dl/go${V}.linux-${GOARCH}.tar.gz" | tar -C /usr/local -xz && \
      ln -s /usr/local/go/bin/go /usr/local/bin/go && \
      ln -s /usr/local/go/bin/gofmt /usr/local/bin/gofmt; \
    fi

# ── Optional: oh-my-opencode-slim (multi-agent orchestration) ────────
# Installs Bun runtime and the oh-my-opencode-slim npm package.
# Runtime activation is controlled by ENABLE_OMOS env var in entrypoint.
# Uses the baseline Bun build (SSE4.2 only) for compatibility with older
# CPUs that lack AVX2 (e.g. Sandy Bridge on OpenStack).
ARG INSTALL_OMOS=false
ARG OMOS_VERSION=latest
RUN if [ "${INSTALL_OMOS}" = "true" ]; then \
      ARCH=$(uname -m) && \
      if [ "$ARCH" = "x86_64" ]; then \
        BUN_ARCH="x64-baseline"; \
      elif [ "$ARCH" = "aarch64" ]; then \
        BUN_ARCH="aarch64"; \
      fi && \
      curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/oven-sh/bun/releases/latest/download/bun-linux-${BUN_ARCH}.zip" -o /tmp/bun.zip && \
      unzip -o /tmp/bun.zip -d /tmp/bun && \
      mv /tmp/bun/bun-linux-${BUN_ARCH}/bun /usr/local/bin/bun && \
      chmod +x /usr/local/bin/bun && \
      ln -sf bun /usr/local/bin/bunx && \
      rm -rf /tmp/bun /tmp/bun.zip && \
      bun --version && \
      test -L /usr/local/bin/bunx && \
      npm install -g oh-my-opencode-slim@${OMOS_VERSION}; \
    fi

# ── Non-root user ────────────────────────────────────────────────────
ARG USER_NAME=developer
ARG USER_UID=1000
ARG USER_GID=1000

RUN groupadd --gid ${USER_GID} ${USER_NAME} && \
    useradd --uid ${USER_UID} --gid ${USER_GID} -m -s /bin/bash ${USER_NAME} && \
    echo "${USER_NAME} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/${USER_NAME}

# Create standard directories
RUN mkdir -p /workspace \
    /home/${USER_NAME}/.config/opencode/skills \
    /home/${USER_NAME}/.agents/skills \
    /home/${USER_NAME}/.local/share/opencode \
    /home/${USER_NAME}/.cache/bash \
    /home/${USER_NAME}/.ssh && \
    chown -R ${USER_NAME}:${USER_NAME} /workspace /home/${USER_NAME}

# ── Shell defaults (bash history, aliases, readline) ─────────────────
# Shipped under /etc/skel-devbox/ rather than copied directly to the
# user's home. The entrypoint copies them to /home/developer/ only if
# the target file does not already exist, so host bind-mounts and
# previously-customized files are never overwritten. Users can restore
# the baked defaults anytime via:
#   cp /etc/skel-devbox/.bash_aliases ~/.bash_aliases
# History itself persists via the devbox-shell-history named volume
# mounted at ~/.cache/bash (HISTFILE points there).
RUN mkdir -p /etc/skel-devbox
COPY rootfs/home/developer/.bash_aliases /etc/skel-devbox/.bash_aliases
COPY rootfs/home/developer/.inputrc      /etc/skel-devbox/.inputrc

# ── Entrypoint ────────────────────────────────────────────────────────
COPY rootfs/usr/local/lib/opencode-devbox/ /usr/local/lib/opencode-devbox/
COPY rootfs/usr/local/bin/ /usr/local/bin/
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
COPY entrypoint-user.sh /usr/local/bin/entrypoint-user.sh
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint-user.sh \
    /usr/local/bin/mempalace-mcp-server \
    /usr/local/lib/opencode-devbox/*.py

# Start as root — entrypoint adjusts UID/GID then drops to developer
WORKDIR /workspace

ENTRYPOINT ["entrypoint.sh"]
CMD ["opencode"]
