Skip to content

Commit c6d1776

Browse files
authored
Ai dev (#54)
1 parent a36a0ec commit c6d1776

File tree

12 files changed

+410
-8
lines changed

12 files changed

+410
-8
lines changed

.github/workflows/build-ai-dev.yml

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Build and publish ai-dev container image
2+
# Triggered on changes to dot_files/ai-dev/ or manual dispatch
3+
# Image published to: ghcr.io/binarypie-dev/ai-dev:latest
4+
5+
name: Build ai-dev Image
6+
7+
concurrency:
8+
group: ${{ github.workflow }}-${{ github.ref }}
9+
cancel-in-progress: true
10+
11+
on:
12+
push:
13+
branches:
14+
- main
15+
paths:
16+
- 'dot_files/ai-dev/**'
17+
- '.github/workflows/build-ai-dev.yml'
18+
pull_request:
19+
paths:
20+
- 'dot_files/ai-dev/**'
21+
- '.github/workflows/build-ai-dev.yml'
22+
workflow_dispatch:
23+
schedule:
24+
# Rebuild daily to get updated packages
25+
- cron: '0 6 * * *'
26+
27+
env:
28+
IMAGE_NAME: ai-dev
29+
IMAGE_REGISTRY: ghcr.io/${{ github.repository_owner }}
30+
31+
jobs:
32+
build:
33+
runs-on: ubuntu-latest-m
34+
permissions:
35+
contents: read
36+
packages: write
37+
38+
steps:
39+
- name: Checkout repository
40+
uses: actions/checkout@v6
41+
42+
- name: Set up Docker Buildx
43+
uses: docker/setup-buildx-action@v3
44+
45+
- name: Log in to GitHub Container Registry
46+
if: github.event_name != 'pull_request'
47+
uses: docker/login-action@v3
48+
with:
49+
registry: ghcr.io
50+
username: ${{ github.actor }}
51+
password: ${{ secrets.GITHUB_TOKEN }}
52+
53+
- name: Extract metadata
54+
id: meta
55+
uses: docker/metadata-action@v5
56+
with:
57+
images: ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}
58+
tags: |
59+
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
60+
type=sha,prefix=
61+
type=ref,event=pr
62+
63+
- name: Build and push
64+
uses: docker/build-push-action@v6
65+
with:
66+
context: dot_files/ai-dev
67+
file: dot_files/ai-dev/Containerfile
68+
push: ${{ github.event_name != 'pull_request' }}
69+
tags: ${{ steps.meta.outputs.tags }}
70+
labels: ${{ steps.meta.outputs.labels }}
71+
cache-from: type=gha
72+
cache-to: type=gha,mode=max
73+
74+
- name: Generate build summary
75+
if: github.event_name != 'pull_request'
76+
run: |
77+
echo "## ai-dev Image Built" >> $GITHUB_STEP_SUMMARY
78+
echo "" >> $GITHUB_STEP_SUMMARY
79+
echo "**Image:** \`${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}:latest\`" >> $GITHUB_STEP_SUMMARY
80+
echo "" >> $GITHUB_STEP_SUMMARY
81+
echo "### Usage" >> $GITHUB_STEP_SUMMARY
82+
echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY
83+
echo "# Run Claude Code" >> $GITHUB_STEP_SUMMARY
84+
echo "podman run --rm -it --user root --security-opt label=disable \\" >> $GITHUB_STEP_SUMMARY
85+
echo " -e HOST_UID=\$(id -u) -e HOST_GID=\$(id -g) -e HOME=\$HOME \\" >> $GITHUB_STEP_SUMMARY
86+
echo " -v \"\$(pwd):\$(pwd):rw\" -w \"\$(pwd)\" \\" >> $GITHUB_STEP_SUMMARY
87+
echo " ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}:latest claude" >> $GITHUB_STEP_SUMMARY
88+
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY

.github/workflows/build-nvim-dev.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Build and publish nvim-dev container image
22
# Triggered on changes to dot_files/nvim/ or manual dispatch
3-
# Image published to: ghcr.io/binarypie/nvim-dev:latest
3+
# Image published to: ghcr.io/binarypie-dev/nvim-dev:latest
44

55
name: Build nvim-dev Image
66

@@ -21,8 +21,8 @@ on:
2121
- '.github/workflows/build-nvim-dev.yml'
2222
workflow_dispatch:
2323
schedule:
24-
# Rebuild weekly to get updated packages
25-
- cron: '0 6 * * 0'
24+
# Rebuild daily to get updated packages
25+
- cron: '0 6 * * *'
2626

2727
env:
2828
IMAGE_NAME: nvim-dev

.github/workflows/check-package-versions.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -253,13 +253,14 @@ jobs:
253253
env:
254254
GH_TOKEN: ${{ github.token }}
255255
run: |
256-
BRANCH="${{ steps.info.outputs.branch }}"
256+
TITLE="${{ steps.info.outputs.title }}"
257257
258-
# Check for PR with this branch
259-
EXISTING=$(gh pr list --head "$BRANCH" --json number --jq '.[0].number' 2>/dev/null) || EXISTING=""
258+
# Check for open PR with the same title
259+
EXISTING=$(gh pr list --state open --search "in:title $TITLE" --json number,title \
260+
--jq ".[] | select(.title == \"$TITLE\") | .number" 2>/dev/null | head -1) || EXISTING=""
260261
261262
if [[ -n "$EXISTING" ]]; then
262-
echo "PR #$EXISTING already exists for this update group"
263+
echo "PR #$EXISTING already exists with title: $TITLE"
263264
echo "exists=true" >> $GITHUB_OUTPUT
264265
else
265266
echo "exists=false" >> $GITHUB_OUTPUT

dot_files/ai-dev/Containerfile

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# AI Development Environment (Sandboxed Podman Container)
2+
# Pre-built image with Claude Code and Gemini CLI
3+
#
4+
# Build: podman build -t ai-dev .
5+
# Run: podman run --rm -it --user 0:0 --security-opt label=disable \
6+
# -e HOME=$HOME -v "$(pwd):$(pwd):rw" -w "$(pwd)" localhost/ai-dev
7+
8+
FROM ghcr.io/binarypie-dev/nvim-dev:latest
9+
10+
# =============================================================================
11+
# LAYER 1: Claude Code (native install)
12+
# =============================================================================
13+
USER linuxbrew
14+
WORKDIR /home/linuxbrew
15+
16+
RUN curl -fsSL https://claude.ai/install.sh | bash
17+
18+
# =============================================================================
19+
# LAYER 2: Gemini CLI via npm
20+
# =============================================================================
21+
RUN npm install -g @google/gemini-cli
22+
23+
# =============================================================================
24+
# LAYER 3: Entrypoint script (sets PATH, execs command)
25+
# =============================================================================
26+
USER root
27+
COPY ai-entrypoint.sh /usr/local/bin/ai-entrypoint.sh
28+
RUN chmod +x /usr/local/bin/ai-entrypoint.sh
29+
30+
# In rootless podman, --user 0:0 maps to host UID (no privilege escalation)
31+
ENTRYPOINT ["/usr/local/bin/ai-entrypoint.sh"]
32+
CMD ["bash"]
33+
34+
# Labels for GitHub Container Registry
35+
LABEL org.opencontainers.image.source="https://github.com/binarypie/hypercube"
36+
LABEL org.opencontainers.image.description="AI development environment with Claude Code and Gemini CLI"
37+
LABEL org.opencontainers.image.licenses="MIT"

dot_files/ai-dev/Justfile

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# AI Development Environment (Sandboxed Podman Container)
2+
# Uses scripts/ for shared logic between local dev and system install
3+
4+
local_image := "localhost/ai-dev"
5+
remote_image := "ghcr.io/binarypie-dev/ai-dev:latest"
6+
7+
# Default recipe - show help
8+
default:
9+
@just --list
10+
11+
# =============================================================================
12+
# Image Building
13+
# =============================================================================
14+
15+
# Build the container image locally
16+
build:
17+
@echo "Building ai-dev image locally..."
18+
podman build -t {{local_image}} .
19+
@echo "Done! Image: {{local_image}}"
20+
21+
# Build without cache
22+
build-no-cache:
23+
@echo "Building ai-dev image (no cache)..."
24+
podman build --no-cache -t {{local_image}} .
25+
@echo "Done! Image: {{local_image}}"
26+
27+
# Pull the remote image
28+
pull:
29+
podman pull {{remote_image}}
30+
31+
# Push to GHCR (requires: podman login ghcr.io)
32+
push:
33+
podman tag {{local_image}} {{remote_image}}
34+
podman push {{remote_image}}
35+
36+
# =============================================================================
37+
# Usage (local image)
38+
# =============================================================================
39+
40+
# Run Claude Code in the current directory
41+
claude *args:
42+
AI_DEV_IMAGE={{local_image}} ./scripts/claude.sh {{args}}
43+
44+
# Run Gemini CLI in the current directory
45+
gemini *args:
46+
AI_DEV_IMAGE={{local_image}} ./scripts/gemini.sh {{args}}
47+
48+
# Enter the container interactively
49+
enter:
50+
AI_DEV_IMAGE={{local_image}} ./scripts/enter.sh
51+
52+
# =============================================================================
53+
# Installation (wrapper scripts to ~/.local/bin)
54+
# =============================================================================
55+
56+
# Install wrapper scripts using remote image
57+
install:
58+
#!/usr/bin/bash
59+
set -euo pipefail
60+
mkdir -p "$HOME/.local/bin"
61+
cp scripts/claude.sh "$HOME/.local/bin/claude"
62+
cp scripts/gemini.sh "$HOME/.local/bin/gemini"
63+
chmod +x "$HOME/.local/bin/claude" "$HOME/.local/bin/gemini"
64+
echo "Installed ~/.local/bin/claude and ~/.local/bin/gemini (image: {{remote_image}})"
65+
66+
# Install wrapper scripts using local image
67+
install-local:
68+
#!/usr/bin/bash
69+
set -euo pipefail
70+
mkdir -p "$HOME/.local/bin"
71+
sed 's|ghcr.io/binarypie-dev/ai-dev:latest|localhost/ai-dev|' scripts/claude.sh > "$HOME/.local/bin/claude"
72+
sed 's|ghcr.io/binarypie-dev/ai-dev:latest|localhost/ai-dev|' scripts/gemini.sh > "$HOME/.local/bin/gemini"
73+
chmod +x "$HOME/.local/bin/claude" "$HOME/.local/bin/gemini"
74+
echo "Installed ~/.local/bin/claude and ~/.local/bin/gemini (image: {{local_image}})"
75+
76+
# Remove wrapper scripts from ~/.local/bin
77+
uninstall:
78+
#!/usr/bin/bash
79+
set -euo pipefail
80+
rm -f "$HOME/.local/bin/claude"
81+
rm -f "$HOME/.local/bin/gemini"
82+
echo "Removed wrapper scripts from ~/.local/bin"
83+
84+
# =============================================================================
85+
# Setup & Cleanup
86+
# =============================================================================
87+
88+
# Full setup: pull image + install wrappers
89+
setup: pull install
90+
@echo ""
91+
@echo "Setup complete! Run 'claude' or 'gemini' to use the AI assistants."
92+
93+
# Remove local image
94+
clean:
95+
#!/usr/bin/bash
96+
set -euo pipefail
97+
podman rmi {{local_image}} 2>/dev/null && echo "Removed {{local_image}}" || echo "No local image to remove"
98+
99+
# =============================================================================
100+
# Testing
101+
# =============================================================================
102+
103+
# Test the built image works correctly
104+
test-build: build
105+
@echo "Testing container..."
106+
AI_DEV_IMAGE={{local_image}} ./scripts/claude.sh --version
107+
AI_DEV_IMAGE={{local_image}} ./scripts/gemini.sh --version
108+
@echo ""
109+
@echo "All tests passed!"
110+
111+
# Debug: show container environment, paths, and auth state
112+
debug:
113+
AI_DEV_IMAGE={{local_image}} ./scripts/enter.sh --ai-dev-debug

dot_files/ai-dev/ai-entrypoint.sh

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#!/usr/bin/bash
2+
set -e
3+
4+
# Fix root's home directory in /etc/passwd to match host $HOME
5+
# This ensures os.homedir() in Node.js (used by claude/gemini) returns the correct path
6+
if [ -n "$HOME" ] && [ "$HOME" != "/root" ]; then
7+
sed -i "s|root:x:0:0:[^:]*:/root:|root:x:0:0:root:$HOME:|" /etc/passwd 2>/dev/null || true
8+
fi
9+
10+
# Symlink claude install paths to where the native installer expects them at runtime
11+
# (installed under /home/linuxbrew at build time, but $HOME differs at runtime)
12+
if [ -n "$HOME" ] && [ "$HOME" != "/home/linuxbrew" ]; then
13+
mkdir -p "$HOME/.local/bin" "$HOME/.local/share"
14+
ln -sf /home/linuxbrew/.local/bin/claude "$HOME/.local/bin/claude" 2>/dev/null || true
15+
ln -sf /home/linuxbrew/.local/share/claude "$HOME/.local/share/claude" 2>/dev/null || true
16+
fi
17+
18+
# Ensure claude auth files are readable within the container
19+
chmod -R a+rX "$HOME/.claude" 2>/dev/null || true
20+
chmod a+rw "$HOME/.claude.json" 2>/dev/null || true
21+
22+
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"
23+
24+
# Debug mode: print environment and auth state
25+
if [ "$1" = "--ai-dev-debug" ]; then
26+
echo "=== ai-dev debug ==="
27+
echo "uid=$(id -u) gid=$(id -g) user=$(whoami 2>/dev/null || echo unknown)"
28+
echo "HOME=$HOME"
29+
echo "PATH=$PATH"
30+
echo ""
31+
echo "=== /etc/passwd root entry ==="
32+
grep "^root:" /etc/passwd
33+
echo ""
34+
echo "=== TTY ==="
35+
ls -la /dev/pts/ 2>/dev/null || echo "no /dev/pts"
36+
echo "tty: $(tty 2>/dev/null || echo 'not a tty')"
37+
echo ""
38+
echo "=== env (GOOGLE_/GEMINI_/ANTHROPIC_) ==="
39+
env | grep -E "^(GOOGLE_|GEMINI_|ANTHROPIC_)" || echo "(none set)"
40+
echo ""
41+
echo "=== $HOME/.gemini/ ==="
42+
ls -la "$HOME/.gemini/" 2>/dev/null || echo "not found at $HOME/.gemini/"
43+
echo ""
44+
echo "=== $HOME/.claude/ ==="
45+
ls -la "$HOME/.claude/" 2>/dev/null || echo "not found at $HOME/.claude/"
46+
echo ""
47+
echo "=== which claude/gemini ==="
48+
which claude 2>/dev/null || echo "claude: not found"
49+
which gemini 2>/dev/null || echo "gemini: not found"
50+
echo ""
51+
echo "=== node os.homedir() ==="
52+
node -e "console.log(require(\"os\").homedir())" 2>/dev/null || echo "node not available"
53+
exit 0
54+
fi
55+
56+
if [ $# -eq 0 ]; then
57+
exec bash
58+
else
59+
exec "$@"
60+
fi

dot_files/ai-dev/scripts/claude.sh

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/bash
2+
set -euo pipefail
3+
4+
IMAGE="${AI_DEV_IMAGE:-ghcr.io/binarypie-dev/ai-dev:latest}"
5+
6+
mkdir -p "$HOME/.claude"
7+
touch "$HOME/.claude.json"
8+
9+
exec podman run --rm -it --init \
10+
--user 0:0 \
11+
--security-opt label=disable \
12+
-e HOME="$HOME" \
13+
-v "$(pwd):$(pwd):rw" \
14+
-v "$HOME/.claude:$HOME/.claude:rw" \
15+
-v "$HOME/.claude.json:$HOME/.claude.json:rw" \
16+
-w "$(pwd)" \
17+
"$IMAGE" \
18+
claude "$@"

dot_files/ai-dev/scripts/enter.sh

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/usr/bin/bash
2+
set -euo pipefail
3+
4+
IMAGE="${AI_DEV_IMAGE:-ghcr.io/binarypie-dev/ai-dev:latest}"
5+
6+
mkdir -p "$HOME/.claude" "$HOME/.gemini"
7+
touch "$HOME/.claude.json"
8+
9+
env_flags=""
10+
for var in $(env | grep -E '^(GOOGLE_|GEMINI_|ANTHROPIC_)' | cut -d= -f1); do
11+
env_flags="$env_flags -e $var"
12+
done
13+
14+
exec podman run --rm -it --init \
15+
--user 0:0 \
16+
--security-opt label=disable \
17+
-e HOME="$HOME" \
18+
$env_flags \
19+
-v "$(pwd):$(pwd):rw" \
20+
-v "$HOME/.claude:$HOME/.claude:rw" \
21+
-v "$HOME/.claude.json:$HOME/.claude.json:rw" \
22+
-v "$HOME/.gemini:$HOME/.gemini:rw" \
23+
-w "$(pwd)" \
24+
"$IMAGE" \
25+
"$@"

0 commit comments

Comments
 (0)