diff --git a/.github/workflows/build-ai-dev.yml b/.github/workflows/build-ai-dev.yml new file mode 100644 index 0000000..9256c6b --- /dev/null +++ b/.github/workflows/build-ai-dev.yml @@ -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 diff --git a/.github/workflows/build-nvim-dev.yml b/.github/workflows/build-nvim-dev.yml index 6183b09..a14ef4f 100644 --- a/.github/workflows/build-nvim-dev.yml +++ b/.github/workflows/build-nvim-dev.yml @@ -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 @@ -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 diff --git a/.github/workflows/check-package-versions.yml b/.github/workflows/check-package-versions.yml index 8ef3aa2..5ef449b 100644 --- a/.github/workflows/check-package-versions.yml +++ b/.github/workflows/check-package-versions.yml @@ -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 diff --git a/dot_files/ai-dev/Containerfile b/dot_files/ai-dev/Containerfile new file mode 100644 index 0000000..689789d --- /dev/null +++ b/dot_files/ai-dev/Containerfile @@ -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" diff --git a/dot_files/ai-dev/Justfile b/dot_files/ai-dev/Justfile new file mode 100644 index 0000000..bb0193a --- /dev/null +++ b/dot_files/ai-dev/Justfile @@ -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 diff --git a/dot_files/ai-dev/ai-entrypoint.sh b/dot_files/ai-dev/ai-entrypoint.sh new file mode 100644 index 0000000..06ca8fa --- /dev/null +++ b/dot_files/ai-dev/ai-entrypoint.sh @@ -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 diff --git a/dot_files/ai-dev/scripts/claude.sh b/dot_files/ai-dev/scripts/claude.sh new file mode 100755 index 0000000..55fcfad --- /dev/null +++ b/dot_files/ai-dev/scripts/claude.sh @@ -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 "$@" diff --git a/dot_files/ai-dev/scripts/enter.sh b/dot_files/ai-dev/scripts/enter.sh new file mode 100755 index 0000000..ebbbcdc --- /dev/null +++ b/dot_files/ai-dev/scripts/enter.sh @@ -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" \ + "$@" diff --git a/dot_files/ai-dev/scripts/gemini.sh b/dot_files/ai-dev/scripts/gemini.sh new file mode 100755 index 0000000..4e7d36c --- /dev/null +++ b/dot_files/ai-dev/scripts/gemini.sh @@ -0,0 +1,22 @@ +#!/usr/bin/bash +set -euo pipefail + +IMAGE="${AI_DEV_IMAGE:-ghcr.io/binarypie-dev/ai-dev:latest}" + +mkdir -p "$HOME/.gemini" + +env_flags="" +for var in $(env | grep -E '^(GOOGLE_|GEMINI_)' | 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/.gemini:$HOME/.gemini:rw" \ + -w "$(pwd)" \ + "$IMAGE" \ + gemini "$@" diff --git a/system_files/shared/etc/distrobox/distrobox.ini b/system_files/shared/etc/distrobox/distrobox.ini index 4741157..4ae2836 100644 --- a/system_files/shared/etc/distrobox/distrobox.ini +++ b/system_files/shared/etc/distrobox/distrobox.ini @@ -2,7 +2,7 @@ # Pre-configured containers for development [nvim-dev] -image=ghcr.io/binarypie/nvim-dev:latest +image=ghcr.io/binarypie-dev/nvim-dev:latest pull=true init=false start_now=false diff --git a/system_files/shared/usr/share/ublue-os/just/62-ai.just b/system_files/shared/usr/share/ublue-os/just/62-ai.just new file mode 100644 index 0000000..0d667a0 --- /dev/null +++ b/system_files/shared/usr/share/ublue-os/just/62-ai.just @@ -0,0 +1,37 @@ +# vim: set ft=make : +# Hypercube AI development environment commands + +ai_scripts := "/usr/share/hypercube/config/ai-dev/scripts" + +# Run Claude Code in the current directory +ai-claude *args: + {{ai_scripts}}/claude.sh {{args}} + +# Run Gemini CLI in the current directory +ai-gemini *args: + {{ai_scripts}}/gemini.sh {{args}} + +# Enter the ai-dev container interactively +ai-dev: + {{ai_scripts}}/enter.sh + +# Install claude and gemini wrapper scripts to ~/.local/bin +ai-setup: + #!/usr/bin/bash + set -euo pipefail + echo "Pulling ai-dev image..." + podman pull ghcr.io/binarypie-dev/ai-dev:latest + mkdir -p "$HOME/.local/bin" + cp {{ai_scripts}}/claude.sh "$HOME/.local/bin/claude" + cp {{ai_scripts}}/gemini.sh "$HOME/.local/bin/gemini" + chmod +x "$HOME/.local/bin/claude" "$HOME/.local/bin/gemini" + echo "" + echo "Setup complete! You can now run 'claude' and 'gemini' directly." + +# Upgrade ai-dev to latest image +ai-upgrade: + #!/usr/bin/bash + set -euo pipefail + echo "Pulling latest ai-dev image..." + podman pull ghcr.io/binarypie-dev/ai-dev:latest + echo "Done! Latest image pulled." diff --git a/system_files/shared/usr/share/ublue-os/justfile b/system_files/shared/usr/share/ublue-os/justfile index c3797a6..52cd2d1 100644 --- a/system_files/shared/usr/share/ublue-os/justfile +++ b/system_files/shared/usr/share/ublue-os/justfile @@ -26,3 +26,4 @@ import "/usr/share/ublue-os/just/50-akmods.just" # ============================================================================= import? "/usr/share/ublue-os/just/60-hypercube.just" import? "/usr/share/ublue-os/just/61-nvim.just" +import? "/usr/share/ublue-os/just/62-ai.just"