Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions .github/workflows/build-ai-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Build and publish ai-dev container image
# Triggered on changes to dot_files/ai-dev/ or manual dispatch
# Image published to: ghcr.io/binarypie-dev/ai-dev:latest

name: Build ai-dev Image

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

on:
push:
branches:
- main
paths:
- 'dot_files/ai-dev/**'
- '.github/workflows/build-ai-dev.yml'
pull_request:
paths:
- 'dot_files/ai-dev/**'
- '.github/workflows/build-ai-dev.yml'
workflow_dispatch:
schedule:
# Rebuild daily to get updated packages
- cron: '0 6 * * *'

env:
IMAGE_NAME: ai-dev
IMAGE_REGISTRY: ghcr.io/${{ github.repository_owner }}

jobs:
build:
runs-on: ubuntu-latest-m
permissions:
contents: read
packages: write

steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
type=sha,prefix=
type=ref,event=pr

- name: Build and push
uses: docker/build-push-action@v6
with:
context: dot_files/ai-dev
file: dot_files/ai-dev/Containerfile
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Generate build summary
if: github.event_name != 'pull_request'
run: |
echo "## ai-dev Image Built" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Image:** \`${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}:latest\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Usage" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY
echo "# Run Claude Code" >> $GITHUB_STEP_SUMMARY
echo "podman run --rm -it --user root --security-opt label=disable \\" >> $GITHUB_STEP_SUMMARY
echo " -e HOST_UID=\$(id -u) -e HOST_GID=\$(id -g) -e HOME=\$HOME \\" >> $GITHUB_STEP_SUMMARY
echo " -v \"\$(pwd):\$(pwd):rw\" -w \"\$(pwd)\" \\" >> $GITHUB_STEP_SUMMARY
echo " ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}:latest claude" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
6 changes: 3 additions & 3 deletions .github/workflows/build-nvim-dev.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Build and publish nvim-dev container image
# Triggered on changes to dot_files/nvim/ or manual dispatch
# Image published to: ghcr.io/binarypie/nvim-dev:latest
# Image published to: ghcr.io/binarypie-dev/nvim-dev:latest

name: Build nvim-dev Image

Expand All @@ -21,8 +21,8 @@ on:
- '.github/workflows/build-nvim-dev.yml'
workflow_dispatch:
schedule:
# Rebuild weekly to get updated packages
- cron: '0 6 * * 0'
# Rebuild daily to get updated packages
- cron: '0 6 * * *'

env:
IMAGE_NAME: nvim-dev
Expand Down
9 changes: 5 additions & 4 deletions .github/workflows/check-package-versions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -253,13 +253,14 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: |
BRANCH="${{ steps.info.outputs.branch }}"
TITLE="${{ steps.info.outputs.title }}"

# Check for PR with this branch
EXISTING=$(gh pr list --head "$BRANCH" --json number --jq '.[0].number' 2>/dev/null) || EXISTING=""
# Check for open PR with the same title
EXISTING=$(gh pr list --state open --search "in:title $TITLE" --json number,title \
--jq ".[] | select(.title == \"$TITLE\") | .number" 2>/dev/null | head -1) || EXISTING=""

if [[ -n "$EXISTING" ]]; then
echo "PR #$EXISTING already exists for this update group"
echo "PR #$EXISTING already exists with title: $TITLE"
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "exists=false" >> $GITHUB_OUTPUT
Expand Down
37 changes: 37 additions & 0 deletions dot_files/ai-dev/Containerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# AI Development Environment (Sandboxed Podman Container)
# Pre-built image with Claude Code and Gemini CLI
#
# Build: podman build -t ai-dev .
# Run: podman run --rm -it --user 0:0 --security-opt label=disable \
# -e HOME=$HOME -v "$(pwd):$(pwd):rw" -w "$(pwd)" localhost/ai-dev

FROM ghcr.io/binarypie-dev/nvim-dev:latest

# =============================================================================
# LAYER 1: Claude Code (native install)
# =============================================================================
USER linuxbrew
WORKDIR /home/linuxbrew

RUN curl -fsSL https://claude.ai/install.sh | bash

# =============================================================================
# LAYER 2: Gemini CLI via npm
# =============================================================================
RUN npm install -g @google/gemini-cli

# =============================================================================
# LAYER 3: Entrypoint script (sets PATH, execs command)
# =============================================================================
USER root
COPY ai-entrypoint.sh /usr/local/bin/ai-entrypoint.sh
RUN chmod +x /usr/local/bin/ai-entrypoint.sh

# In rootless podman, --user 0:0 maps to host UID (no privilege escalation)
ENTRYPOINT ["/usr/local/bin/ai-entrypoint.sh"]
CMD ["bash"]

# Labels for GitHub Container Registry
LABEL org.opencontainers.image.source="https://github.com/binarypie/hypercube"
LABEL org.opencontainers.image.description="AI development environment with Claude Code and Gemini CLI"
LABEL org.opencontainers.image.licenses="MIT"
113 changes: 113 additions & 0 deletions dot_files/ai-dev/Justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# AI Development Environment (Sandboxed Podman Container)
# Uses scripts/ for shared logic between local dev and system install

local_image := "localhost/ai-dev"
remote_image := "ghcr.io/binarypie-dev/ai-dev:latest"

# Default recipe - show help
default:
@just --list

# =============================================================================
# Image Building
# =============================================================================

# Build the container image locally
build:
@echo "Building ai-dev image locally..."
podman build -t {{local_image}} .
@echo "Done! Image: {{local_image}}"

# Build without cache
build-no-cache:
@echo "Building ai-dev image (no cache)..."
podman build --no-cache -t {{local_image}} .
@echo "Done! Image: {{local_image}}"

# Pull the remote image
pull:
podman pull {{remote_image}}

# Push to GHCR (requires: podman login ghcr.io)
push:
podman tag {{local_image}} {{remote_image}}
podman push {{remote_image}}

# =============================================================================
# Usage (local image)
# =============================================================================

# Run Claude Code in the current directory
claude *args:
AI_DEV_IMAGE={{local_image}} ./scripts/claude.sh {{args}}

# Run Gemini CLI in the current directory
gemini *args:
AI_DEV_IMAGE={{local_image}} ./scripts/gemini.sh {{args}}

# Enter the container interactively
enter:
AI_DEV_IMAGE={{local_image}} ./scripts/enter.sh

# =============================================================================
# Installation (wrapper scripts to ~/.local/bin)
# =============================================================================

# Install wrapper scripts using remote image
install:
#!/usr/bin/bash
set -euo pipefail
mkdir -p "$HOME/.local/bin"
cp scripts/claude.sh "$HOME/.local/bin/claude"
cp scripts/gemini.sh "$HOME/.local/bin/gemini"
chmod +x "$HOME/.local/bin/claude" "$HOME/.local/bin/gemini"
echo "Installed ~/.local/bin/claude and ~/.local/bin/gemini (image: {{remote_image}})"

# Install wrapper scripts using local image
install-local:
#!/usr/bin/bash
set -euo pipefail
mkdir -p "$HOME/.local/bin"
sed 's|ghcr.io/binarypie-dev/ai-dev:latest|localhost/ai-dev|' scripts/claude.sh > "$HOME/.local/bin/claude"
sed 's|ghcr.io/binarypie-dev/ai-dev:latest|localhost/ai-dev|' scripts/gemini.sh > "$HOME/.local/bin/gemini"
chmod +x "$HOME/.local/bin/claude" "$HOME/.local/bin/gemini"
echo "Installed ~/.local/bin/claude and ~/.local/bin/gemini (image: {{local_image}})"

# Remove wrapper scripts from ~/.local/bin
uninstall:
#!/usr/bin/bash
set -euo pipefail
rm -f "$HOME/.local/bin/claude"
rm -f "$HOME/.local/bin/gemini"
echo "Removed wrapper scripts from ~/.local/bin"

# =============================================================================
# Setup & Cleanup
# =============================================================================

# Full setup: pull image + install wrappers
setup: pull install
@echo ""
@echo "Setup complete! Run 'claude' or 'gemini' to use the AI assistants."

# Remove local image
clean:
#!/usr/bin/bash
set -euo pipefail
podman rmi {{local_image}} 2>/dev/null && echo "Removed {{local_image}}" || echo "No local image to remove"

# =============================================================================
# Testing
# =============================================================================

# Test the built image works correctly
test-build: build
@echo "Testing container..."
AI_DEV_IMAGE={{local_image}} ./scripts/claude.sh --version
AI_DEV_IMAGE={{local_image}} ./scripts/gemini.sh --version
@echo ""
@echo "All tests passed!"

# Debug: show container environment, paths, and auth state
debug:
AI_DEV_IMAGE={{local_image}} ./scripts/enter.sh --ai-dev-debug
60 changes: 60 additions & 0 deletions dot_files/ai-dev/ai-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/bash
set -e

# Fix root's home directory in /etc/passwd to match host $HOME
# This ensures os.homedir() in Node.js (used by claude/gemini) returns the correct path
if [ -n "$HOME" ] && [ "$HOME" != "/root" ]; then
sed -i "s|root:x:0:0:[^:]*:/root:|root:x:0:0:root:$HOME:|" /etc/passwd 2>/dev/null || true
fi

# Symlink claude install paths to where the native installer expects them at runtime
# (installed under /home/linuxbrew at build time, but $HOME differs at runtime)
if [ -n "$HOME" ] && [ "$HOME" != "/home/linuxbrew" ]; then
mkdir -p "$HOME/.local/bin" "$HOME/.local/share"
ln -sf /home/linuxbrew/.local/bin/claude "$HOME/.local/bin/claude" 2>/dev/null || true
ln -sf /home/linuxbrew/.local/share/claude "$HOME/.local/share/claude" 2>/dev/null || true
fi

# Ensure claude auth files are readable within the container
chmod -R a+rX "$HOME/.claude" 2>/dev/null || true
chmod a+rw "$HOME/.claude.json" 2>/dev/null || true

export PATH="$HOME/.local/bin:/home/linuxbrew/.local/bin:/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin:/home/linuxbrew/.npm-global/bin:/home/linuxbrew/go/bin:/home/linuxbrew/.cargo/bin:$PATH"

# Debug mode: print environment and auth state
if [ "$1" = "--ai-dev-debug" ]; then
echo "=== ai-dev debug ==="
echo "uid=$(id -u) gid=$(id -g) user=$(whoami 2>/dev/null || echo unknown)"
echo "HOME=$HOME"
echo "PATH=$PATH"
echo ""
echo "=== /etc/passwd root entry ==="
grep "^root:" /etc/passwd
echo ""
echo "=== TTY ==="
ls -la /dev/pts/ 2>/dev/null || echo "no /dev/pts"
echo "tty: $(tty 2>/dev/null || echo 'not a tty')"
echo ""
echo "=== env (GOOGLE_/GEMINI_/ANTHROPIC_) ==="
env | grep -E "^(GOOGLE_|GEMINI_|ANTHROPIC_)" || echo "(none set)"
echo ""
echo "=== $HOME/.gemini/ ==="
ls -la "$HOME/.gemini/" 2>/dev/null || echo "not found at $HOME/.gemini/"
echo ""
echo "=== $HOME/.claude/ ==="
ls -la "$HOME/.claude/" 2>/dev/null || echo "not found at $HOME/.claude/"
echo ""
echo "=== which claude/gemini ==="
which claude 2>/dev/null || echo "claude: not found"
which gemini 2>/dev/null || echo "gemini: not found"
echo ""
echo "=== node os.homedir() ==="
node -e "console.log(require(\"os\").homedir())" 2>/dev/null || echo "node not available"
exit 0
fi

if [ $# -eq 0 ]; then
exec bash
else
exec "$@"
fi
18 changes: 18 additions & 0 deletions dot_files/ai-dev/scripts/claude.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/bash
set -euo pipefail

IMAGE="${AI_DEV_IMAGE:-ghcr.io/binarypie-dev/ai-dev:latest}"

mkdir -p "$HOME/.claude"
touch "$HOME/.claude.json"

exec podman run --rm -it --init \
--user 0:0 \
--security-opt label=disable \
-e HOME="$HOME" \
-v "$(pwd):$(pwd):rw" \
-v "$HOME/.claude:$HOME/.claude:rw" \
-v "$HOME/.claude.json:$HOME/.claude.json:rw" \
-w "$(pwd)" \
"$IMAGE" \
claude "$@"
25 changes: 25 additions & 0 deletions dot_files/ai-dev/scripts/enter.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/bash
set -euo pipefail

IMAGE="${AI_DEV_IMAGE:-ghcr.io/binarypie-dev/ai-dev:latest}"

mkdir -p "$HOME/.claude" "$HOME/.gemini"
touch "$HOME/.claude.json"

env_flags=""
for var in $(env | grep -E '^(GOOGLE_|GEMINI_|ANTHROPIC_)' | cut -d= -f1); do
env_flags="$env_flags -e $var"
done

exec podman run --rm -it --init \
--user 0:0 \
--security-opt label=disable \
-e HOME="$HOME" \
$env_flags \
-v "$(pwd):$(pwd):rw" \
-v "$HOME/.claude:$HOME/.claude:rw" \
-v "$HOME/.claude.json:$HOME/.claude.json:rw" \
-v "$HOME/.gemini:$HOME/.gemini:rw" \
-w "$(pwd)" \
"$IMAGE" \
"$@"
Loading