├── package.json ├── .devcontainer ├── .env.example ├── devcontainer.json ├── docker-compose.yml ├── README.md ├── scripts │ ├── set-github-env.sh │ ├── setup-git-auto.sh │ └── git-profile └── Dockerfile ├── .npmignore ├── .gitignore ├── BUG.md ├── GIT_SETUP.md ├── README.md ├── Setup-VibeTunnel-And-Tailscale-in-container.md └── .github └── workflows └── claude.yml /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "claude-dev-container", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "stop": "docker compose -f .devcontainer/docker-compose.yml down" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC" 13 | } 14 | -------------------------------------------------------------------------------- /.devcontainer/.env.example: -------------------------------------------------------------------------------- 1 | # Tailscale Auth Key 2 | # Get this from your Tailscale admin console: Settings > Keys 3 | # Make sure to enable "Reusable" when generating the key 4 | TS_AUTHKEY=tskey-auth-your-key-here 5 | 6 | # Timezone (optional, defaults to America/Los_Angeles) 7 | TZ=America/Los_Angeles 8 | 9 | # Tunnel user password (defaults to 'nodepassword' if not set) 10 | TUNNEL_PASSWORD=your-secure-password-here -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Development directories 2 | docs/ 3 | 4 | # Documentation files 5 | NOTES.md 6 | 7 | # Development files 8 | .git/ 9 | .gitignore 10 | .eslintrc 11 | .prettierrc 12 | 13 | # Test files 14 | tests/ 15 | coverage/ 16 | 17 | # Editor files 18 | .cursor/ 19 | .vscode/ 20 | .idea/ 21 | *.swp 22 | *.swo 23 | 24 | # Debug logs 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # Environment files 30 | .env* 31 | .env.* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .cursor 8 | # TS map files 9 | **/*.map 10 | 11 | # generated content 12 | .devcontainer/.env 13 | 14 | # testing 15 | /coverage 16 | 17 | # next.js 18 | /.next/ 19 | /out/ 20 | 21 | # production 22 | /build 23 | 24 | # misc 25 | .DS_Store 26 | *.pem 27 | 28 | # debug 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | 33 | # local env files 34 | .env*.local 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | 43 | # Local Netlify folder 44 | .netlify 45 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Claude Code Sandbox", 3 | "dockerComposeFile": ["docker-compose.yml"], 4 | "service": "devcontainer", 5 | "shutdownAction": "stopCompose", 6 | "customizations": { 7 | "vscode": { 8 | "extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "eamodio.gitlens"], 9 | "settings": { 10 | "editor.formatOnSave": true, 11 | "editor.defaultFormatter": "esbenp.prettier-vscode", 12 | "editor.codeActionsOnSave": { 13 | "source.fixAll.eslint": "explicit" 14 | }, 15 | "terminal.integrated.defaultProfile.linux": "zsh", 16 | "terminal.integrated.profiles.linux": { 17 | "bash": { 18 | "path": "bash", 19 | "icon": "terminal-bash" 20 | }, 21 | "zsh": { 22 | "path": "zsh" 23 | } 24 | } 25 | } 26 | } 27 | }, 28 | "remoteUser": "node", 29 | "workspaceFolder": "/workspace", 30 | "postCreateCommand": "npm install && sudo mkdir -p /opt/google/chrome && sudo ln -sf /home/node/.cache/ms-playwright/chromium-*/chrome-linux/chrome /opt/google/chrome/chrome", 31 | "postStartCommand": "bash /workspace/.devcontainer/scripts/setup-git-auto.sh && /usr/local/bin/set-github-env.sh", 32 | "forwardPorts": [5173], 33 | "portsAttributes": { 34 | "5173": { 35 | "label": "Vite App", 36 | "onAutoForward": "notify" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | tailscale: 3 | image: tailscale/tailscale:latest 4 | container_name: devcontainer-tailscale 5 | hostname: claude-code-dev 6 | env_file: 7 | - .env 8 | environment: 9 | - TS_AUTHKEY=${TS_AUTHKEY} 10 | - TS_STATE_DIR=/var/lib/tailscale 11 | - TS_USERSPACE=false 12 | volumes: 13 | - tailscale-state:/var/lib/tailscale 14 | - /dev/net/tun:/dev/net/tun 15 | cap_add: 16 | - NET_ADMIN 17 | - SYS_MODULE 18 | restart: unless-stopped 19 | 20 | devcontainer: 21 | build: 22 | context: . 23 | dockerfile: Dockerfile 24 | args: 25 | TZ: '${TZ:-America/Los_Angeles}' 26 | TUNNEL_PASSWORD: '${TUNNEL_PASSWORD:-nodepassword}' 27 | container_name: claude-code-devcontainer 28 | network_mode: service:tailscale 29 | env_file: 30 | - .env 31 | depends_on: 32 | - tailscale 33 | volumes: 34 | - ..:/workspace:cached 35 | - claude-code-bashhistory:/commandhistory 36 | - ${HOME}/.claude:/home/node/.claude 37 | - ${HOME}/.gitconfig:/home/node/.gitconfig-host:ro 38 | environment: 39 | - NODE_OPTIONS=--max-old-space-size=4096 40 | - CLAUDE_CONFIG_DIR=/home/node/.claude 41 | - POWERLEVEL9K_DISABLE_GITSTATUS=true 42 | working_dir: /workspace 43 | command: sleep infinity 44 | user: node 45 | 46 | volumes: 47 | tailscale-state: 48 | driver: local 49 | claude-code-bashhistory: 50 | driver: local 51 | -------------------------------------------------------------------------------- /.devcontainer/README.md: -------------------------------------------------------------------------------- 1 | # Dev Container 2 | 3 | This dev container automatically sets up your development environment with Node.js, Deno, Playwright, and Git configuration. 4 | 5 | ## Git Profile Auto-Setup 6 | 7 | The container automatically configures Git on startup using one of these methods (in priority order): 8 | 9 | ### 1. Environment Variables (Recommended) 10 | 11 | Set these environment variables before starting the container: 12 | 13 | ```bash 14 | export GIT_USER_NAME="Your Name" 15 | export GIT_USER_EMAIL="your.email@example.com" 16 | export GIT_GITHUB_USER="yourusername" # Optional 17 | export GIT_PROFILE_NAME="work" # Optional, defaults to "default" 18 | ``` 19 | 20 | ### 2. Host Git Config 21 | 22 | Your host's `~/.gitconfig` file is automatically copied if available. 23 | 24 | ### 3. Default Profile 25 | 26 | If you have a profile named "default" (or set `GIT_DEFAULT_PROFILE`), it will be applied automatically. 27 | 28 | ### 4. Manual Setup 29 | 30 | Use the `git-profile` command to create and manage profiles: 31 | 32 | ```bash 33 | git-profile create work 34 | git-profile use work 35 | ``` 36 | 37 | ## Available Commands 38 | 39 | - `git-profile` - Manage Git profiles (create, list, use, delete) 40 | 41 | ## TODO 42 | 43 | - Copy config files in like https://github.com/Vakarva/dev-builder/blob/13231243c0352b825895b8d992456b5c898da913/Containerfile#L19-L23 44 | - Node via nvm? https://github.com/Vakarva/dev-builder/blob/13231243c0352b825895b8d992456b5c898da913/Containerfile#L52-L58 45 | - Python via uv? https://github.com/Vakarva/dev-builder/blob/13231243c0352b825895b8d992456b5c898da913/Containerfile#L52-L58 46 | 47 | ## verify VT 48 | 49 | ``` 50 | After rebuild, you can test with: 51 | - tailscale status (to verify Tailscale connection) 52 | - vibetunnel --help (to verify VibeTunnel installation) 53 | ``` 54 | -------------------------------------------------------------------------------- /.devcontainer/scripts/set-github-env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Extract GitHub owner and repo from git remote origin 4 | if cd /workspace && git remote get-url origin >/dev/null 2>&1; then 5 | REMOTE_URL=$(git remote get-url origin 2>/dev/null) 6 | if [[ $REMOTE_URL =~ github\.com ]]; then 7 | # Extract owner and repo from GitHub URL 8 | if [[ $REMOTE_URL =~ github\.com[/:]([^/]+)/([^/]+)(\.git)?$ ]]; then 9 | export GITHUB_OWNER="${BASH_REMATCH[1]}" 10 | export GITHUB_REPO="${BASH_REMATCH[2]}" 11 | 12 | # Remove .git suffix if present 13 | GITHUB_REPO="${GITHUB_REPO%.git}" 14 | 15 | # Remove any existing entries from shell profiles to avoid duplicates 16 | sed -i '/export GITHUB_OWNER=/d' /home/node/.bashrc 17 | sed -i '/export GITHUB_REPO=/d' /home/node/.bashrc 18 | sed -i '/export GITHUB_OWNER=/d' /home/node/.zshrc 19 | sed -i '/export GITHUB_REPO=/d' /home/node/.zshrc 20 | 21 | # Set in environment files for persistence 22 | echo "export GITHUB_OWNER=\"$GITHUB_OWNER\"" >> /home/node/.bashrc 23 | echo "export GITHUB_REPO=\"$GITHUB_REPO\"" >> /home/node/.bashrc 24 | echo "export GITHUB_OWNER=\"$GITHUB_OWNER\"" >> /home/node/.zshrc 25 | echo "export GITHUB_REPO=\"$GITHUB_REPO\"" >> /home/node/.zshrc 26 | 27 | echo "GitHub environment variables set:" 28 | echo " GITHUB_OWNER=$GITHUB_OWNER" 29 | echo " GITHUB_REPO=$GITHUB_REPO" 30 | else 31 | echo "Could not parse GitHub URL: $REMOTE_URL" 32 | export GITHUB_OWNER="unknown" 33 | export GITHUB_REPO="unknown" 34 | fi 35 | else 36 | echo "Remote URL is not a GitHub repository: $REMOTE_URL" 37 | export GITHUB_OWNER="unknown" 38 | export GITHUB_REPO="unknown" 39 | fi 40 | else 41 | echo "No git remote found or /workspace is not a git repository" 42 | export GITHUB_OWNER="unknown" 43 | export GITHUB_REPO="unknown" 44 | fi -------------------------------------------------------------------------------- /BUG.md: -------------------------------------------------------------------------------- 1 | Important! Check if you run the latest version and update + retest before submitting this report. 2 | We are moving fast, issues without a version number or with an outdated number will be ignored. 3 | 4 | **Describe the bug** 5 | 6 | ``` 7 | ➜ /workspace git:(master) ✗ vt claude 8 | Warning: authenticate-pam native module not found. PAM authentication will not work. 9 | ╭───────────────────────────────────────────────────╮ 10 | │ ✻ Welcome to Claude Code! │ 11 | │ │ 12 | │ /help for help, /status for your current setup │ 13 | │ │ 14 | │ cwd: /workspace │ 15 | ╰───────────────────────────────────────────────────╯ 16 | 17 | > test 18 | 19 | ✢ Discombobulating… (0s · ↑ 0 tokens · esc to interrupt) 20 | 21 | ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ 22 | │ > │ 23 | ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ 24 | ? for shortcuts ◯ 25 | 26 | 2025-07-17T23:05:31.890Z ERROR [[SRV] cli] Uncaught exception: {} 27 | 2025-07-17T23:05:31.891Z ERROR [[SRV] cli] Stack trace: SyntaxError: Invalid regular expression: /[^ 28 | ]**[^ 29 | ]*to\s+interrupt[^ 30 | ]*/gi: Nothing to repeat 31 | at new RegExp () 32 | at Object.Ci [as parseStatus] (/usr/lib/node_modules/vibetunnel/lib/vibetunnel-cli:14:852) 33 | at Pt.processOutput (/usr/lib/node_modules/vibetunnel/lib/vibetunnel-cli:17:1064) 34 | at /usr/lib/node_modules/vibetunnel/lib/vibetunnel-cli:17:25713 35 | at EventEmitter2.fire (/usr/lib/node_modules/vibetunnel/node-pty/lib/eventEmitter2.js:36:22) 36 | at ReadStream. (/usr/lib/node_modules/vibetunnel/node-pty/lib/terminal.js:71:43) 37 | at ReadStream.emit (node:events:507:28) 38 | at addChunk (node:internal/streams/readable:559:12) 39 | at readableAddChunkPushByteMode (node:internal/streams/readable:510:3) 40 | at Readable.push (node:internal/streams/readable:390:5) 41 | ➜ /workspace git:(master) ✗ 42 | ``` 43 | 44 | Appears to be this regex https://github.com/amantus-ai/vibetunnel/blob/main/web/src/server/utils/activity-detector.ts#L179C1-L182C7 45 | 46 | ``` 47 | const statusPattern = new RegExp( 48 | `[^\n]*${escapeRegex(indicator)}[^\n]*to\\s+interrupt[^\n]*`, 49 | 'gi' 50 | ); 51 | ``` 52 | 53 | **To Reproduce** 54 | Steps to reproduce the behavior: 55 | 56 | 1. Go to '...' 57 | 2. Click on '....' 58 | 3. Scroll down to '....' 59 | 4. See error 60 | 61 | **Expected behavior** 62 | A clear and concise description of what you expected to happen. 63 | 64 | **Screenshots** 65 | If applicable, add screenshots to help explain your problem. 66 | 67 | **Desktop (please complete the following information):** 68 | 69 | - OS: [e.g. iOS] 70 | - Browser [e.g. chrome, safari] 71 | - Version [e.g. 22] 72 | 73 | **Smartphone (please complete the following information):** 74 | 75 | - Device: [e.g. iPhone6] 76 | - OS: [e.g. iOS8.1] 77 | - Browser [e.g. stock browser, safari] 78 | - Version [e.g. 22] 79 | 80 | **Additional context** 81 | Add any other context about the problem here. 82 | -------------------------------------------------------------------------------- /.devcontainer/scripts/setup-git-auto.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Automatic Git Profile Setup 4 | # This script runs during container startup to automatically configure git 5 | 6 | PROFILES_DIR="/home/node/.git-profiles" 7 | CURRENT_PROFILE_FILE="/home/node/.current-git-profile" 8 | GIT_PROFILE_SCRIPT="/workspace/.devcontainer/scripts/git-profile" 9 | 10 | # Colors for output 11 | GREEN='\033[0;32m' 12 | YELLOW='\033[1;33m' 13 | BLUE='\033[0;34m' 14 | NC='\033[0m' # No Color 15 | 16 | # Function to create a default profile from environment variables 17 | create_profile_from_env() { 18 | local profile_name="${GIT_PROFILE_NAME:-default}" 19 | local user_name="${GIT_USER_NAME}" 20 | local user_email="${GIT_USER_EMAIL}" 21 | local github_user="${GIT_GITHUB_USER}" 22 | 23 | if [ -n "$user_name" ] && [ -n "$user_email" ]; then 24 | echo -e "${BLUE}Creating Git profile '$profile_name' from environment variables${NC}" 25 | 26 | mkdir -p "$PROFILES_DIR" 27 | local profile_file="$PROFILES_DIR/$profile_name" 28 | 29 | cat > "$profile_file" << EOF 30 | # Git Profile: $profile_name (auto-created) 31 | user.name=$user_name 32 | user.email=$user_email 33 | EOF 34 | 35 | if [ -n "$github_user" ]; then 36 | echo "github.user=$github_user" >> "$profile_file" 37 | fi 38 | 39 | echo -e "${GREEN}Profile '$profile_name' created and activated${NC}" 40 | 41 | # Apply the profile 42 | git config --global user.name "$user_name" 43 | git config --global user.email "$user_email" 44 | if [ -n "$github_user" ]; then 45 | git config --global github.user "$github_user" 46 | fi 47 | 48 | # Save as current profile 49 | echo "$profile_name" > "$CURRENT_PROFILE_FILE" 50 | 51 | return 0 52 | fi 53 | 54 | return 1 55 | } 56 | 57 | # Function to auto-apply a default profile if it exists 58 | apply_default_profile() { 59 | local default_profile="${GIT_DEFAULT_PROFILE:-default}" 60 | local profile_file="$PROFILES_DIR/$default_profile" 61 | 62 | if [ -f "$profile_file" ]; then 63 | echo -e "${BLUE}Applying default Git profile: $default_profile${NC}" 64 | 65 | # Use the git-profile script to apply it 66 | if [ -x "$GIT_PROFILE_SCRIPT" ]; then 67 | "$GIT_PROFILE_SCRIPT" use "$default_profile" 68 | fi 69 | 70 | return 0 71 | fi 72 | 73 | return 1 74 | } 75 | 76 | # Function to check if git is already configured 77 | git_is_configured() { 78 | local user_name=$(git config --global user.name 2>/dev/null) 79 | local user_email=$(git config --global user.email 2>/dev/null) 80 | 81 | [ -n "$user_name" ] && [ -n "$user_email" ] 82 | } 83 | 84 | # Main setup logic 85 | echo -e "${BLUE}Setting up Git configuration...${NC}" 86 | 87 | # Ensure safe directory is set 88 | git config --global --add safe.directory /workspace 89 | 90 | # Copy host gitconfig if available (existing behavior) 91 | if [ -f /home/node/.gitconfig-host ]; then 92 | cp /home/node/.gitconfig-host /home/node/.gitconfig 93 | echo -e "${GREEN}Copied Git configuration from host${NC}" 94 | elif create_profile_from_env; then 95 | # Created profile from environment variables 96 | : 97 | elif apply_default_profile; then 98 | # Applied existing default profile 99 | : 100 | elif git_is_configured; then 101 | echo -e "${GREEN}Git is already configured${NC}" 102 | else 103 | echo -e "${YELLOW}No Git configuration found.${NC}" 104 | echo -e "${YELLOW}To set up your Git identity:${NC}" 105 | echo -e "${YELLOW} 1. Run: git-profile create ${NC}" 106 | echo -e "${YELLOW} 2. Or set environment variables: GIT_USER_NAME, GIT_USER_EMAIL${NC}" 107 | fi 108 | 109 | # Make git-profile script executable and available in PATH 110 | if [ -f "$GIT_PROFILE_SCRIPT" ]; then 111 | chmod +x "$GIT_PROFILE_SCRIPT" 112 | 113 | # Add to PATH if not already there 114 | if ! echo "$PATH" | grep -q "/workspace/.devcontainer/scripts"; then 115 | echo 'export PATH="/workspace/.devcontainer/scripts:$PATH"' >> /home/node/.bashrc 116 | echo 'export PATH="/workspace/.devcontainer/scripts:$PATH"' >> /home/node/.zshrc 2>/dev/null || true 117 | fi 118 | fi 119 | 120 | echo -e "${GREEN}Git setup complete${NC}" -------------------------------------------------------------------------------- /GIT_SETUP.md: -------------------------------------------------------------------------------- 1 | # Git Configuration in Dev Container 2 | 3 | This dev container is now configured to automatically handle Git credentials and support multiple Git profiles. 4 | 5 | ## How It Works 6 | 7 | ### Automatic Git Configuration 8 | 9 | 1. **Host Credentials**: The container automatically mounts your host's `.gitconfig` file and uses it if available 10 | 2. **Fallback**: If no host configuration is found, you'll need to set up your Git identity manually 11 | 3. **Safe Directory**: The workspace is automatically added as a safe directory 12 | 13 | ### Multiple Git Profiles 14 | 15 | Use the built-in `git-profile` command to manage multiple Git identities: 16 | 17 | ```bash 18 | # Create a new profile 19 | git-profile create work 20 | git-profile create personal 21 | 22 | # List all profiles 23 | git-profile list 24 | 25 | # Switch between profiles 26 | git-profile use work 27 | git-profile use personal 28 | 29 | # Show current profile 30 | git-profile current 31 | 32 | # Edit a profile 33 | git-profile edit work 34 | 35 | # Delete a profile 36 | git-profile delete old-profile 37 | ``` 38 | 39 | ## Quick Start 40 | 41 | If you're getting git commit errors, here's how to fix it: 42 | 43 | ### Option 1: Use Host Configuration (Recommended) 44 | Your host's `.gitconfig` should automatically be available in the container. If not, restart the container. 45 | 46 | ### Option 2: Create a Profile 47 | ```bash 48 | git-profile create main 49 | # Follow the prompts to enter your name and email 50 | git-profile use main 51 | ``` 52 | 53 | ### Option 3: Manual Configuration 54 | ```bash 55 | git config --global user.name "Your Name" 56 | git config --global user.email "your.email@example.com" 57 | ``` 58 | 59 | ## NPM Packages for Git Profile Management 60 | 61 | If you prefer npm-based solutions, here are popular packages: 62 | 63 | ### 1. **git-user-switch** 64 | ```bash 65 | npm install -g git-user-switch 66 | ``` 67 | Simple CLI for switching git users globally or per repository. 68 | 69 | ### 2. **gitconfig** 70 | ```bash 71 | npm install -g gitconfig 72 | ``` 73 | Programmatic API for reading/writing git config. 74 | 75 | ### 3. **git-user** 76 | ```bash 77 | npm install -g git-user 78 | ``` 79 | Lightweight tool for managing git user configurations. 80 | 81 | ### 4. **git-profile-manager** 82 | ```bash 83 | npm install -g git-profile-manager 84 | ``` 85 | Feature-rich profile manager with templates and project-specific configs. 86 | 87 | ## GitHub CLI Integration 88 | 89 | The container includes GitHub CLI (`gh`). After setting up git, authenticate with: 90 | 91 | ```bash 92 | gh auth login 93 | ``` 94 | 95 | This will help with repository operations and maintain consistency between git and GitHub identities. 96 | 97 | ## Environment Variables 98 | 99 | The container sets these environment variables: 100 | - `GITHUB_OWNER`: Currently set to `bailejl` (you may want to change this) 101 | - `GITHUB_REPO`: Currently set to `Product-Outcomes` (you may want to change this) 102 | 103 | ## Troubleshooting 104 | 105 | ### Problem: "Please tell me who you are" error 106 | **Solution**: Run `git-profile create myprofile` and follow the prompts, then `git-profile use myprofile` 107 | 108 | ### Problem: Host .gitconfig not mounted 109 | **Solution**: Ensure your host has a `.gitconfig` file in your home directory, then rebuild the container 110 | 111 | ### Problem: Permission issues with git operations 112 | **Solution**: The container automatically adds the workspace as a safe directory. If issues persist, run: 113 | ```bash 114 | git config --global --add safe.directory /workspace 115 | ``` 116 | 117 | ## Advanced Usage 118 | 119 | ### Project-specific Git Configuration 120 | 121 | You can also set repository-specific configuration: 122 | 123 | ```bash 124 | # In your project directory 125 | git config user.name "Project Specific Name" 126 | git config user.email "project@example.com" 127 | ``` 128 | 129 | This overrides global settings for that specific repository. 130 | 131 | ### SSH Key Setup 132 | 133 | To use SSH keys with GitHub: 134 | 135 | 1. Mount your SSH keys in the devcontainer.json: 136 | ```json 137 | "mounts": [ 138 | "source=${localEnv:HOME}/.ssh,target=/home/node/.ssh,type=bind,readonly" 139 | ] 140 | ``` 141 | 142 | 2. Or create new SSH keys in the container: 143 | ```bash 144 | ssh-keygen -t ed25519 -C "your_email@example.com" 145 | gh ssh-key add ~/.ssh/id_ed25519.pub 146 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Claude Dev Container 2 | 3 | A comprehensive development container setup with Node.js, Deno, Bun.js, Playwright, Tailscale, and VibeTunnel for Claude Code development. 4 | 5 | ## Features 6 | 7 | - **Node.js v24** - Latest Node.js runtime 8 | - **Deno v1.46.3** - Modern JavaScript/TypeScript runtime 9 | - **Bun.js** - Fast JavaScript runtime and package manager 10 | - **Playwright** - Browser automation with Chromium pre-installed 11 | - **Tailscale** - Secure network connectivity 12 | - **VibeTunnel** - Local development tunneling 13 | - **GitHub CLI** - Command-line GitHub integration 14 | - **Claude Code** - Anthropic's CLI tool pre-installed 15 | - **Git Profile Management** - Automated Git configuration 16 | - **VS Code Extensions** - ESLint, Prettier, GitLens pre-configured 17 | 18 | ## Quick Start 19 | 20 | 1. Open in VS Code with Dev Containers extension (from Cursor "Anysphere") 21 | 2. Container will automatically build and configure 22 | 3. Git will be auto-configured based on your environment 23 | 24 | ## Container Architecture 25 | 26 | The setup uses Docker Compose with two services: 27 | 28 | ### Tailscale Service 29 | - Provides secure networking capabilities 30 | - Requires `TS_AUTHKEY` environment variable 31 | - Hostname: `claude-code-dev` 32 | 33 | ### Development Container 34 | - Based on Ubuntu 22.04 35 | - Network shares with Tailscale service 36 | - User: `node` with sudo privileges 37 | - Working directory: `/workspace` 38 | 39 | ## Pre-installed Tools 40 | 41 | - **Runtime**: Node.js v24, Deno v1.46.3, Bun.js, Python3 42 | - **CLI Tools**: GitHub CLI, Claude Code, Gemini CLI, VibeTunnel 43 | - **Development**: git, curl, wget, vim, fzf 44 | - **Browser Testing**: Playwright with Chromium, X11 support 45 | - **Package Managers**: npm, pnpm, bun 46 | 47 | ## Environment Variables 48 | 49 | Create a `.env` file in the `.devcontainer` directory: 50 | 51 | ```bash 52 | # Required for Tailscale 53 | TS_AUTHKEY=your_tailscale_auth_key 54 | 55 | # Optional Git configuration 56 | GIT_USER_NAME="Your Name" 57 | GIT_USER_EMAIL="your.email@example.com" 58 | GIT_GITHUB_USER="yourusername" 59 | GIT_PROFILE_NAME="work" 60 | 61 | # Optional timezone 62 | TZ=America/Los_Angeles 63 | ``` 64 | 65 | ## Git Configuration 66 | 67 | The container automatically sets up Git using multiple methods (in priority order): 68 | 69 | 1. **Environment Variables** - Set `GIT_USER_NAME`, `GIT_USER_EMAIL`, etc. 70 | 2. **Host Git Config** - Copies your `~/.gitconfig` if available 71 | 3. **Default Profile** - Uses existing "default" profile 72 | 4. **Manual Setup** - Use `git-profile` command 73 | 74 | ### Git Profile Commands 75 | 76 | ```bash 77 | git-profile create work # Create new profile 78 | git-profile use work # Switch to profile 79 | git-profile list # List all profiles 80 | git-profile delete work # Delete profile 81 | ``` 82 | 83 | ## VS Code Configuration 84 | 85 | Pre-configured with: 86 | - **Default Formatter**: Prettier 87 | - **Linting**: ESLint with auto-fix on save 88 | - **Git Integration**: GitLens extension 89 | - **Terminal**: zsh with Oh My Zsh (robbyrussell theme) 90 | - **Port Forwarding**: Port 5173 for Vite development 91 | 92 | ## Aliases 93 | 94 | The container includes helpful aliases: 95 | - `yolo` - Claude with skip permissions 96 | - `gyolo` - Gemini with yolo mode 97 | - `tailscale` - Execute Tailscale commands in container 98 | 99 | ## Volume Mounts 100 | 101 | - **Workspace**: `..:/workspace:cached` 102 | - **Command History**: Persistent zsh history 103 | - **Claude Config**: `~/.claude` directory mounted 104 | - **Git Config**: Host `.gitconfig` mounted read-only 105 | 106 | ## Development Workflow 107 | 108 | 1. **Start Container**: Open in VS Code or run `docker-compose up` 109 | 2. **Install Dependencies**: `npm install` (runs automatically) 110 | 3. **Start Development**: Use pre-configured tools and extensions 111 | 4. **Browser Testing**: Playwright ready with Chromium 112 | 5. **Secure Access**: Tailscale for remote connectivity 113 | 114 | ## Networking 115 | 116 | The container uses Tailscale for secure networking, allowing: 117 | - Remote access to development environment 118 | - Secure tunneling with VibeTunnel 119 | - Connection to private networks 120 | 121 | ## Troubleshooting 122 | 123 | - **Tailscale**: Verify `TS_AUTHKEY` is set and valid 124 | - **Git**: Check environment variables or use `git-profile` commands 125 | - **Playwright**: Run `npx playwright install` if browsers missing 126 | - **Permissions**: User `node` has sudo access with password `nodepassword` 127 | 128 | For VibeTunnel and Tailscale verification: 129 | ```bash 130 | tailscale status # Check Tailscale connection 131 | vibetunnel --help # Verify VibeTunnel installation 132 | ``` 133 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/vscode/devcontainers/base:ubuntu-22.04 2 | 3 | # Set shell with pipefail for better error handling 4 | SHELL ["/bin/bash", "-o", "pipefail", "-c"] 5 | 6 | # Install essential tools and Playwright dependencies 7 | RUN apt-get update && apt-get install -y --no-install-recommends \ 8 | git \ 9 | curl \ 10 | wget \ 11 | python3 \ 12 | make \ 13 | g++ \ 14 | sudo \ 15 | unzip \ 16 | fzf \ 17 | vim \ 18 | # PAM authentication dependencies 19 | libpam0g-dev \ 20 | libpam-modules \ 21 | # X11 and display dependencies for headful browser testing 22 | xvfb \ 23 | x11-utils \ 24 | # Playwright dependencies 25 | libnss3 \ 26 | libnspr4 \ 27 | libatk1.0-0 \ 28 | libatk-bridge2.0-0 \ 29 | libcups2 \ 30 | libdrm2 \ 31 | libdbus-1-3 \ 32 | libxkbcommon0 \ 33 | libatspi2.0-0 \ 34 | libx11-6 \ 35 | libxcomposite1 \ 36 | libxdamage1 \ 37 | libxext6 \ 38 | libxfixes3 \ 39 | libxrandr2 \ 40 | libgbm1 \ 41 | libxcb1 \ 42 | libxss1 \ 43 | libgtk-3-0 \ 44 | libpango-1.0-0 \ 45 | libcairo2 \ 46 | libasound2 \ 47 | # Clean up 48 | && rm -rf /var/lib/apt/lists/* \ 49 | # Create fzf configuration files if they don't exist 50 | && mkdir -p /usr/share/doc/fzf/examples \ 51 | && echo "# fzf key bindings" > /usr/share/doc/fzf/examples/key-bindings.zsh \ 52 | && echo "# fzf completion" > /usr/share/doc/fzf/examples/completion.zsh 53 | 54 | # Install Deno (required for claude-flow) 55 | # Using a more robust installation method with proper error handling 56 | RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \ 57 | && rm -rf /var/lib/apt/lists/* \ 58 | && export DENO_INSTALL="/usr/local" \ 59 | && curl -fsSL https://deno.land/install.sh | sh -s v1.46.3 \ 60 | || (echo "Deno installation failed, trying alternative method..." \ 61 | && wget -qO- https://github.com/denoland/deno/releases/latest/download/deno-x86_64-unknown-linux-gnu.zip -O /tmp/deno.zip \ 62 | && unzip -q /tmp/deno.zip -d /usr/local/bin/ \ 63 | && rm /tmp/deno.zip) \ 64 | && chmod +x /usr/local/bin/deno 65 | 66 | # Install Node.js v24 67 | RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \ 68 | && apt-get install -y --no-install-recommends nodejs \ 69 | && rm -rf /var/lib/apt/lists/* 70 | 71 | # Install Bun.js (latest version) 72 | RUN curl -fsSL https://bun.sh/install | bash \ 73 | && mv ~/.bun/bin/bun /usr/local/bin/bun \ 74 | && chmod +x /usr/local/bin/bun 75 | 76 | # Install GitHub CLI 77 | RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | tee /usr/share/keyrings/githubcli-archive-keyring.gpg > /dev/null \ 78 | && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ 79 | && apt-get update && apt-get install -y --no-install-recommends gh \ 80 | && rm -rf /var/lib/apt/lists/* 81 | 82 | # Create node user and configure for development 83 | # Check if group/user already exist and handle accordingly 84 | ARG TUNNEL_PASSWORD=nodepassword 85 | RUN (getent group node || groupadd node) \ 86 | && (id -u node &>/dev/null || useradd -g node -s /bin/bash -m node) \ 87 | && echo "node:${TUNNEL_PASSWORD}" | chpasswd \ 88 | && echo node ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/node \ 89 | && chmod 0440 /etc/sudoers.d/node 90 | 91 | ARG USERNAME=node 92 | 93 | # Persist shell history for zsh 94 | RUN mkdir -p /commandhistory \ 95 | && touch /commandhistory/.zsh_history \ 96 | && chown -R $USERNAME:$USERNAME /commandhistory \ 97 | && chmod 755 /commandhistory \ 98 | && chmod 644 /commandhistory/.zsh_history 99 | 100 | # Set `DEVCONTAINER` environment variable to help with orientation 101 | ENV DEVCONTAINER=true 102 | 103 | # Install global npm packages with cache mount 104 | RUN --mount=type=cache,target=/root/.npm \ 105 | npm install -g pnpm @anthropic-ai/claude-code@latest @google/gemini-cli vibetunnel@latest 106 | 107 | # Fix npm cache permissions for node user after global installs 108 | RUN mkdir -p /home/node/.npm && chown -R node:node /home/node/.npm 109 | # VScode in web browser 110 | # RUN curl -fsSL https://code-server.dev/install.sh | sh 111 | 112 | # Copy git-profile script and make it executable 113 | COPY scripts/git-profile /usr/local/bin/git-profile 114 | RUN chmod +x /usr/local/bin/git-profile 115 | 116 | # Create directories for Playwright 117 | RUN mkdir -p /home/node/.cache/ms-playwright \ 118 | && chown -R node:node /home/node/.cache 119 | 120 | # Switch to node user 121 | USER node 122 | 123 | # Set npm cache directory 124 | ENV NPM_CONFIG_CACHE=/home/node/.npm 125 | 126 | # Set the default shell to zsh rather than sh 127 | ENV SHELL=/bin/zsh 128 | 129 | # Default robbyrussell theme 130 | RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v1.2.0/zsh-in-docker.sh)" -- \ 131 | -t robbyrussell \ 132 | -p git \ 133 | -p fzf \ 134 | -a "source /usr/share/doc/fzf/examples/key-bindings.zsh" \ 135 | -a "source /usr/share/doc/fzf/examples/completion.zsh" \ 136 | -a "export HISTFILE=/commandhistory/.zsh_history" \ 137 | -a "export HISTSIZE=10000" \ 138 | -a "export SAVEHIST=10000" \ 139 | -a "setopt SHARE_HISTORY" \ 140 | -a "setopt APPEND_HISTORY" \ 141 | -a "alias yolo='claude --dangerously-skip-permissions'" \ 142 | -a "alias gyolo='gemini --yolo'" \ 143 | -a "alias tailscale='docker exec devcontainer-tailscale tailscale'" \ 144 | -a "# Auto-set GitHub environment variables from git remote" \ 145 | -a "/usr/local/bin/set-github-env.sh >/dev/null 2>&1 || true" \ 146 | -x 147 | 148 | # Copy GitHub environment setup script (as root) 149 | USER root 150 | COPY scripts/set-github-env.sh /usr/local/bin/set-github-env.sh 151 | RUN chmod +x /usr/local/bin/set-github-env.sh 152 | USER node 153 | 154 | # Set default GitHub environment variables (will be overridden by script at runtime) 155 | ENV GITHUB_OWNER=unknown 156 | ENV GITHUB_REPO=unknown 157 | 158 | # # Set up aliases and environment info in profile 159 | # RUN alias swarm-status="/github status" \ 160 | # && alias swarm-tasks="/github tasks" \ 161 | # && alias swarm-claim="/github claim" \ 162 | # && alias swarm-update="/github update" \ 163 | # && echo "echo GITHUB_OWNER: \$(echo \$GITHUB_OWNER)" >> ~/.profile \ 164 | # && echo "echo GITHUB_REPO: \$(echo \$GITHUB_REPO)" >> ~/.profile \ 165 | # && echo "echo CLAUDE_SWARM_ID: \$(echo \$CLAUDE_SWARM_ID)" >> ~/.profile 166 | 167 | # Create cache directories with proper permissions first 168 | RUN mkdir -p /home/node/.cache/ms-playwright /home/node/.npm && \ 169 | chown -R node:node /home/node/.cache /home/node/.npm 170 | 171 | # Switch back to root to install global packages 172 | USER root 173 | 174 | # Install Playwright globally as root 175 | RUN npm install -g @playwright/test 176 | 177 | # Fix npm cache permissions again after Playwright install 178 | RUN mkdir -p /home/node/.npm && chown -R node:node /home/node/.npm 179 | 180 | # Switch back to node user for browser installation 181 | USER node 182 | RUN npx playwright install chromium 183 | 184 | # Create Chrome symlink for MCP server compatibility (needed for ARM64) 185 | # Switch back to root to create system directories 186 | USER root 187 | RUN mkdir -p /opt/google/chrome 188 | USER node 189 | RUN sudo ln -sf /home/node/.cache/ms-playwright/chromium-*/chrome-linux/chrome /opt/google/chrome/chrome 190 | 191 | # Set working directory 192 | WORKDIR /workspace -------------------------------------------------------------------------------- /.devcontainer/scripts/git-profile: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Git Profile Manager 4 | # Helps manage multiple git identities in the dev container 5 | 6 | PROFILES_DIR="/home/node/.git-profiles" 7 | CURRENT_PROFILE_FILE="/home/node/.current-git-profile" 8 | 9 | # Colors for output 10 | RED='\033[0;31m' 11 | GREEN='\033[0;32m' 12 | YELLOW='\033[1;33m' 13 | BLUE='\033[0;34m' 14 | NC='\033[0m' # No Color 15 | 16 | usage() { 17 | echo -e "${BLUE}Git Profile Manager${NC}" 18 | echo 19 | echo "Usage: git-profile [command] [options]" 20 | echo 21 | echo "Commands:" 22 | echo " list List all saved profiles" 23 | echo " create Create a new profile" 24 | echo " use Switch to a profile" 25 | echo " current Show current profile" 26 | echo " delete Delete a profile" 27 | echo " edit Edit a profile" 28 | echo " help Show this help message" 29 | echo 30 | echo "Examples:" 31 | echo " git-profile create work" 32 | echo " git-profile use personal" 33 | echo " git-profile list" 34 | } 35 | 36 | init_profiles_dir() { 37 | mkdir -p "$PROFILES_DIR" 38 | } 39 | 40 | list_profiles() { 41 | init_profiles_dir 42 | echo -e "${BLUE}Available Git Profiles:${NC}" 43 | echo 44 | 45 | if [ ! "$(ls -A "$PROFILES_DIR" 2>/dev/null)" ]; then 46 | echo -e "${YELLOW}No profiles found. Create one with: git-profile create ${NC}" 47 | return 48 | fi 49 | 50 | current_profile="" 51 | if [ -f "$CURRENT_PROFILE_FILE" ]; then 52 | current_profile=$(cat "$CURRENT_PROFILE_FILE") 53 | fi 54 | 55 | for profile in "$PROFILES_DIR"/*; do 56 | if [ -f "$profile" ]; then 57 | profile_name=$(basename "$profile") 58 | if [ "$profile_name" = "$current_profile" ]; then 59 | echo -e " ${GREEN}● $profile_name${NC} (current)" 60 | else 61 | echo " $profile_name" 62 | fi 63 | fi 64 | done 65 | } 66 | 67 | create_profile() { 68 | local name="$1" 69 | if [ -z "$name" ]; then 70 | echo -e "${RED}Error: Profile name is required${NC}" 71 | echo "Usage: git-profile create " 72 | return 1 73 | fi 74 | 75 | init_profiles_dir 76 | local profile_file="$PROFILES_DIR/$name" 77 | 78 | if [ -f "$profile_file" ]; then 79 | echo -e "${YELLOW}Profile '$name' already exists${NC}" 80 | return 1 81 | fi 82 | 83 | echo -e "${BLUE}Creating profile '$name'${NC}" 84 | echo 85 | 86 | read -p "Enter your name: " user_name 87 | read -p "Enter your email: " user_email 88 | 89 | # Optionally ask for GitHub username 90 | read -p "Enter GitHub username (optional): " github_user 91 | 92 | cat > "$profile_file" << EOF 93 | # Git Profile: $name 94 | user.name=$user_name 95 | user.email=$user_email 96 | EOF 97 | 98 | if [ -n "$github_user" ]; then 99 | echo "github.user=$github_user" >> "$profile_file" 100 | fi 101 | 102 | echo -e "${GREEN}Profile '$name' created successfully${NC}" 103 | echo "Use it with: git-profile use $name" 104 | } 105 | 106 | use_profile() { 107 | local name="$1" 108 | if [ -z "$name" ]; then 109 | echo -e "${RED}Error: Profile name is required${NC}" 110 | echo "Usage: git-profile use " 111 | return 1 112 | fi 113 | 114 | local profile_file="$PROFILES_DIR/$name" 115 | 116 | if [ ! -f "$profile_file" ]; then 117 | echo -e "${RED}Error: Profile '$name' not found${NC}" 118 | echo "Available profiles:" 119 | list_profiles 120 | return 1 121 | fi 122 | 123 | # Apply the profile 124 | while IFS='=' read -r key value; do 125 | if [[ ! $key =~ ^#.*$ ]] && [ -n "$key" ] && [ -n "$value" ]; then 126 | git config --global "$key" "$value" 127 | fi 128 | done < "$profile_file" 129 | 130 | # Save current profile 131 | echo "$name" > "$CURRENT_PROFILE_FILE" 132 | 133 | echo -e "${GREEN}Switched to profile '$name'${NC}" 134 | show_current_config 135 | } 136 | 137 | show_current() { 138 | if [ -f "$CURRENT_PROFILE_FILE" ]; then 139 | current_profile=$(cat "$CURRENT_PROFILE_FILE") 140 | echo -e "${BLUE}Current profile: ${GREEN}$current_profile${NC}" 141 | else 142 | echo -e "${YELLOW}No active profile${NC}" 143 | fi 144 | 145 | show_current_config 146 | } 147 | 148 | show_current_config() { 149 | echo 150 | echo -e "${BLUE}Current git configuration:${NC}" 151 | local user_name=$(git config --global user.name 2>/dev/null) 152 | local user_email=$(git config --global user.email 2>/dev/null) 153 | local github_user=$(git config --global github.user 2>/dev/null) 154 | 155 | if [ -n "$user_name" ]; then 156 | echo " Name: $user_name" 157 | fi 158 | 159 | if [ -n "$user_email" ]; then 160 | echo " Email: $user_email" 161 | fi 162 | 163 | if [ -n "$github_user" ]; then 164 | echo " GitHub: $github_user" 165 | fi 166 | 167 | if [ -z "$user_name" ] && [ -z "$user_email" ]; then 168 | echo -e " ${YELLOW}No git identity configured${NC}" 169 | fi 170 | } 171 | 172 | delete_profile() { 173 | local name="$1" 174 | if [ -z "$name" ]; then 175 | echo -e "${RED}Error: Profile name is required${NC}" 176 | echo "Usage: git-profile delete " 177 | return 1 178 | fi 179 | 180 | local profile_file="$PROFILES_DIR/$name" 181 | 182 | if [ ! -f "$profile_file" ]; then 183 | echo -e "${RED}Error: Profile '$name' not found${NC}" 184 | return 1 185 | fi 186 | 187 | read -p "Are you sure you want to delete profile '$name'? (y/N): " confirm 188 | if [[ $confirm =~ ^[Yy]$ ]]; then 189 | rm "$profile_file" 190 | 191 | # If this was the current profile, clear it 192 | if [ -f "$CURRENT_PROFILE_FILE" ] && [ "$(cat "$CURRENT_PROFILE_FILE")" = "$name" ]; then 193 | rm "$CURRENT_PROFILE_FILE" 194 | fi 195 | 196 | echo -e "${GREEN}Profile '$name' deleted${NC}" 197 | else 198 | echo "Operation cancelled" 199 | fi 200 | } 201 | 202 | edit_profile() { 203 | local name="$1" 204 | if [ -z "$name" ]; then 205 | echo -e "${RED}Error: Profile name is required${NC}" 206 | echo "Usage: git-profile edit " 207 | return 1 208 | fi 209 | 210 | local profile_file="$PROFILES_DIR/$name" 211 | 212 | if [ ! -f "$profile_file" ]; then 213 | echo -e "${RED}Error: Profile '$name' not found${NC}" 214 | return 1 215 | fi 216 | 217 | ${EDITOR:-nano} "$profile_file" 218 | echo -e "${GREEN}Profile '$name' updated${NC}" 219 | } 220 | 221 | # Main command handling 222 | case "$1" in 223 | list|ls) 224 | list_profiles 225 | ;; 226 | create|new) 227 | create_profile "$2" 228 | ;; 229 | use|switch) 230 | use_profile "$2" 231 | ;; 232 | current|show) 233 | show_current 234 | ;; 235 | delete|remove|rm) 236 | delete_profile "$2" 237 | ;; 238 | edit) 239 | edit_profile "$2" 240 | ;; 241 | help|--help|-h) 242 | usage 243 | ;; 244 | "") 245 | usage 246 | ;; 247 | *) 248 | echo -e "${RED}Error: Unknown command '$1'${NC}" 249 | echo 250 | usage 251 | exit 1 252 | ;; 253 | esac -------------------------------------------------------------------------------- /Setup-VibeTunnel-And-Tailscale-in-container.md: -------------------------------------------------------------------------------- 1 | Can vibe tunnel https://github.com/amantus-ai/vibetunnel be run from inside a docker container? 2 | 3 | Edit 4 | favicon 5 | 6 | Fetched GitHub - amantus-ai/vibetunnel: Turn any browser into your terminal & command your agents on the go. 7 | github.com 8 | 9 | vibetunnel docker container architecture 10 | 10 results 11 | 12 | Based on the GitHub repository, VibeTunnel is a macOS native app written in Swift/SwiftUI with a high-performance Rust server for terminal management 13 | GitHub - amantus-ai/vibetunnel: Turn any browser into your terminal & command your agents on the go. 14 | . It's specifically designed for Mac and requires macOS to run. 15 | 16 | No, VibeTunnel cannot run from inside a Docker container because: 17 | 18 | Native macOS dependency - It's a native macOS app written in Swift/SwiftUI 19 | GitHub - amantus-ai/vibetunnel: Turn any browser into your terminal & command your agents on the go. 20 | System integration - It needs to access your Mac's terminal sessions and menu bar 21 | Architecture - Uses a Rust server component that manages terminal sessions on macOS 22 | GitHub - amantus-ai/vibetunnel: Turn any browser into your terminal & command your agents on the go. 23 | Docker containers share the host kernel, so you'd need macOS as the host system anyway. Even then, the app is designed to run natively on macOS, not in a containerized environment. 24 | 25 | If you need terminal access from containers, consider alternatives like SSH tunneling or web-based terminal solutions designed for containerized environments. 26 | 27 | npm Package (Linux & Headless Systems) 28 | 29 | For Linux servers, Docker containers, or headless macOS systems, install via npm: 30 | 31 | npm install -g vibetunnel 32 | 33 | Edit 34 | You're absolutely right! I missed that important detail. 35 | 36 | Yes, VibeTunnel can run from inside a Docker container using the npm package: 37 | 38 | bash 39 | npm install -g vibetunnel 40 | This npm version is specifically designed for: 41 | 42 | Linux servers 43 | Docker containers 44 | Headless macOS systems 45 | The npm package provides the core functionality without the native macOS GUI, making it perfect for containerized environments where you need terminal tunneling capabilities. 46 | 47 | So to answer your original question: Yes, you can run VibeTunnel inside a Docker container using the npm package version. 48 | 49 | What about tailscale how would you have that in a docker image? 50 | 51 | Edit 52 | 53 | tailscale docker container installation 54 | 10 results 55 | 56 | For Tailscale in Docker, you have several approaches: 57 | 58 | Official Tailscale Docker Image 59 | Tailscale has an official Docker image available in Docker Hub and GitHub Packages 60 | Using Tailscale with Docker · Tailscale Docs 61 | : 62 | 63 | javascript 64 | // Basic docker-compose.yml with VibeTunnel + Tailscale 65 | version: '3.8' 66 | services: 67 | tailscale: 68 | image: tailscale/tailscale:latest 69 | container_name: vibetunnel-tailscale 70 | hostname: vibetunnel-server 71 | environment: - TS_AUTHKEY=${TS_AUTHKEY} - TS_STATE_DIR=/var/lib/tailscale - TS_USERSPACE=false 72 | volumes: - tailscale-state:/var/lib/tailscale - /dev/net/tun:/dev/net/tun 73 | cap_add: - NET_ADMIN - SYS_MODULE 74 | restart: unless-stopped 75 | 76 | vibetunnel: 77 | image: node:18-alpine 78 | container_name: vibetunnel 79 | network_mode: service:tailscale 80 | working_dir: /app 81 | command: sh -c "npm install -g vibetunnel && vibetunnel" 82 | restart: unless-stopped 83 | 84 | volumes: 85 | tailscale-state: 86 | driver: local 87 | Key Configuration Points 88 | Environment Variables: 89 | 90 | TS_AUTHKEY - auth key for authentication 91 | Using Tailscale with Docker · Tailscale Docs 92 | TS_STATE_DIR - directory where Tailscale state is stored, needs to persist across restarts 93 | Using Tailscale with Docker · Tailscale Docs 94 | Volumes: 95 | 96 | Mount /var/lib/tailscale to persist state across container restarts 97 | Using Tailscale with Docker · Tailscale Docs 98 | Mount /dev/net/tun for kernel networking 99 | Capabilities: 100 | 101 | Add NET_ADMIN and SYS_MODULE capabilities 102 | Using Tailscale with Docker · Tailscale Docs 103 | Networking: 104 | 105 | Use network_mode: service:tailscale to route all traffic through the Tailscale container (sidecar pattern) 106 | Add a Docker container · Tailscale Docs 107 | Get your auth key from the Tailscale admin console under Settings > Keys. 108 | 109 | So docker compose runs 2 images together? 110 | 111 | Edit 112 | Yes, exactly! Docker Compose can run multiple containers together as a single application stack 113 | Add a Docker container · Tailscale Docs 114 | . 115 | 116 | In the example I showed: 117 | 118 | javascript 119 | // Two separate containers working together 120 | services: 121 | tailscale: // Container 1: Tailscale networking 122 | image: tailscale/tailscale:latest 123 | 124 | vibetunnel: // Container 2: VibeTunnel app 125 | image: node:18-alpine 126 | network_mode: service:tailscale // Routes through container 1 127 | How it works: 128 | 129 | tailscale container handles the VPN connection 130 | vibetunnel container runs the actual app 131 | network_mode: service:tailscale routes all VibeTunnel traffic through the Tailscale container - this is called a "sidecar" pattern 132 | Contain your excitement: A deep dive into using Tailscale with Docker 133 | Benefits: 134 | 135 | Separation of concerns (networking vs app logic) 136 | Can reuse the Tailscale container for other services 137 | Easy to manage both together with one command: docker compose up 138 | This is common for microservices - one container per service, all coordinated by Docker Compose. 139 | 140 | How do you set TS_AUTHKEY? 141 | 142 | Edit 143 | You can set TS_AUTHKEY in several ways: 144 | 145 | 1. Environment File (.env) 146 | Create a .env file in your docker-compose directory: 147 | 148 | javascript 149 | // .env file 150 | TS_AUTHKEY=tskey-auth-kJDnLXE8sT11CNTRL-example123456789 151 | Then reference it in docker-compose: 152 | 153 | javascript 154 | environment: 155 | 156 | - TS_AUTHKEY=${TS_AUTHKEY} 157 | 158 | 2. Direct in docker-compose.yml 159 | javascript 160 | environment: 161 | 162 | - TS_AUTHKEY=tskey-auth-kJDnLXE8sT11CNTRL-example123456 163 | 164 | 3. Shell Environment Variable 165 | bash 166 | export TS_AUTHKEY=tskey-auth-kJDnLXE8sT11CNTRL-example123456 167 | docker compose up 168 | Getting the Auth Key 169 | To get your auth key, open the Keys page of the admin console, select Generate auth key, provide a key name, toggle Reusable to on, and select Generate key 170 | Add a Docker container · Tailscale Docs 171 | . 172 | 173 | Recommended: Use the .env file approach - keeps secrets out of your compose file and git history. Add .env to your .gitignore. 174 | 175 | Does docker compose work with dev containers? 176 | 177 | Edit 178 | 179 | docker compose dev containers vscode devcontainer 180 | 10 results 181 | 182 | Yes! Dev containers work seamlessly with Docker Compose 183 | Connect to multiple containers 184 | . You can reference Docker Compose files in your dev container configuration. 185 | 186 | How it works: 187 | In your .devcontainer/devcontainer.json: 188 | 189 | javascript 190 | { 191 | "name": "VibeTunnel Dev Container", 192 | "dockerComposeFile": ["../docker-compose.yml"], 193 | "service": "vibetunnel", 194 | "workspaceFolder": "/workspace" 195 | } 196 | Your docker-compose.yml: 197 | 198 | javascript 199 | // docker-compose.yml 200 | version: '3.8' 201 | services: 202 | tailscale: 203 | image: tailscale/tailscale:latest 204 | environment: - TS_AUTHKEY=${TS_AUTHKEY} 205 | volumes: - /dev/net/tun:/dev/net/tun 206 | cap_add: - NET_ADMIN 207 | 208 | vibetunnel: 209 | image: node:18-alpine 210 | network_mode: service:tailscale 211 | volumes: - ..:/workspace // Mount your project code 212 | command: sleep infinity // Keep container running 213 | Benefits: 214 | Docker Compose orchestrates multiple containers that work together 215 | Use Docker Compose 216 | VS Code connects to one service while other services (like Tailscale) run alongside 217 | Developing inside a Container 218 | Development environment matches production setup 219 | You can develop in a production-like environment with all dependencies running 220 | How to use VS Code Dev Containers with your docker compose deployment for efficient development and deployment 221 | When you open in dev container, VS Code connects to the vibetunnel service while Tailscale runs in its own container. 222 | -------------------------------------------------------------------------------- /.github/workflows/claude.yml: -------------------------------------------------------------------------------- 1 | name: Claude Code 2 | 3 | on: 4 | # PR related events 5 | pull_request_target: 6 | types: [opened, synchronize, reopened] 7 | pull_request_review_comment: 8 | types: [created] 9 | pull_request_review: 10 | types: [submitted] 11 | # Issue related events (added) 12 | issues: 13 | types: [opened, assigned] 14 | issue_comment: 15 | types: [created] 16 | 17 | # Concurrency control (one run per Issue/PR) 18 | concurrency: 19 | group: claude-${{ github.repository }}-${{ github.event.number || github.run_id }} 20 | cancel-in-progress: false 21 | 22 | jobs: 23 | setup: 24 | # Security-focused conditional execution (full support for Issues and PRs) 25 | if: | 26 | ( 27 | github.event_name == 'pull_request_target' && 28 | ( 29 | github.event.pull_request.head.repo.full_name == github.repository || 30 | contains(fromJSON('["COLLABORATOR", "MEMBER", "OWNER"]'), github.event.pull_request.author_association) 31 | ) && 32 | contains(github.event.pull_request.body, '@claude') 33 | ) || 34 | ( 35 | github.event_name == 'issue_comment' && 36 | ( 37 | github.event.sender.login == github.repository_owner || 38 | contains(fromJSON('["COLLABORATOR", "MEMBER", "OWNER"]'), github.event.comment.author_association) 39 | ) && 40 | contains(github.event.comment.body, '@claude') 41 | ) || 42 | ( 43 | github.event_name == 'issues' && 44 | ( 45 | github.event.sender.login == github.repository_owner || 46 | contains(fromJSON('["COLLABORATOR", "MEMBER", "OWNER"]'), github.event.issue.author_association) 47 | ) && 48 | ( 49 | contains(github.event.issue.body, '@claude') || 50 | contains(github.event.issue.title, '@claude') 51 | ) 52 | ) || 53 | ( 54 | github.event_name == 'pull_request_review_comment' && 55 | ( 56 | github.event.sender.login == github.repository_owner || 57 | contains(fromJSON('["COLLABORATOR", "MEMBER", "OWNER"]'), github.event.comment.author_association) 58 | ) && 59 | contains(github.event.comment.body, '@claude') 60 | ) || 61 | ( 62 | github.event_name == 'pull_request_review' && 63 | ( 64 | github.event.sender.login == github.repository_owner || 65 | contains(fromJSON('["COLLABORATOR", "MEMBER", "OWNER"]'), github.event.review.author_association) 66 | ) && 67 | contains(github.event.review.body, '@claude') 68 | ) 69 | 70 | runs-on: ubuntu-latest 71 | timeout-minutes: 2 72 | permissions: 73 | # 📁 Content management (highest permissions) 74 | contents: write 75 | pull-requests: write 76 | issues: write 77 | discussions: write 78 | # 🔧 Development & CI/CD management 79 | actions: write 80 | checks: write 81 | statuses: write 82 | pages: write 83 | deployments: write 84 | # 📦 Package & security management 85 | packages: write 86 | security-events: write 87 | # 🎯 Project management 88 | repository-projects: write 89 | # 🆔 Authentication & token management 90 | id-token: write 91 | # Outputs 92 | outputs: 93 | should-continue: ${{ steps.should-continue.outputs.should-continue }} 94 | issue-number: ${{ steps.context-info.outputs.issue-number }} 95 | pr-number: ${{ steps.context-info.outputs.pr-number }} 96 | head-ref: ${{ steps.context-info.outputs.head-ref }} 97 | base-ref: ${{ steps.context-info.outputs.base-ref }} 98 | head-sha: ${{ steps.context-info.outputs.head-sha }} 99 | is-pr: ${{ steps.context-info.outputs.is-pr }} 100 | trigger-text: ${{ steps.context-info.outputs.trigger-text }} 101 | has-linked-pr: ${{ steps.context-info.outputs.has-linked-pr }} 102 | status-comment-id: ${{ steps.find_comment.outputs.comment-id || steps.create_comment.outputs.comment-id }} 103 | ###################### 104 | # Setup steps 105 | ###################### 106 | steps: 107 | - name: Get Context Information 108 | id: context-info 109 | uses: actions/github-script@v7 110 | with: 111 | script: | 112 | let issueNumber, prNumber, headRef, baseRef, headSha 113 | let triggerText = '' 114 | let hasLinkedPR = false 115 | let isPR = false 116 | 117 | if (context.eventName === 'pull_request_target') { 118 | // When a PR is created or updated 119 | isPR = true 120 | issueNumber = context.payload.pull_request.number 121 | prNumber = context.payload.pull_request.number 122 | headRef = context.payload.pull_request.head.ref 123 | baseRef = context.payload.pull_request.base.ref 124 | headSha = context.payload.pull_request.head.sha 125 | triggerText = context.payload.pull_request.body 126 | 127 | console.log(`PR #${prNumber}: ${baseRef} <- ${headRef} (${headSha})`) 128 | 129 | } else if (context.eventName === 'issues') { 130 | // When an Issue is created or assigned 131 | isPR = false 132 | issueNumber = context.payload.issue.number 133 | triggerText = `${context.payload.issue.title} ${context.payload.issue.body}` 134 | 135 | console.log(`Issue #${issueNumber} created`) 136 | 137 | } else if (context.eventName === 'issue_comment') { 138 | // Issue/PR comment 139 | issueNumber = context.payload.issue.number 140 | triggerText = context.payload.comment.body 141 | 142 | if (context.payload.issue.pull_request) { 143 | // Comment on a PR 144 | isPR = true 145 | try { 146 | const pr = await github.rest.pulls.get({ 147 | owner: context.repo.owner, 148 | repo: context.repo.repo, 149 | pull_number: issueNumber 150 | }) 151 | prNumber = issueNumber 152 | headRef = pr.data.head.ref 153 | baseRef = pr.data.base.ref 154 | headSha = pr.data.head.sha 155 | 156 | console.log(`PR Comment #${prNumber}: ${baseRef} <- ${headRef}`) 157 | } catch (error) { 158 | console.error('Error fetching PR info:', error) 159 | // In case of error, treat as a regular Issue 160 | isPR = false 161 | } 162 | } else { 163 | // Regular Issue comment - check for existing linked PRs 164 | isPR = false 165 | 166 | try { 167 | // Get timeline events to find linked pull requests 168 | const { data: timeline } = await github.rest.issues.listEventsForTimeline({ 169 | owner: context.repo.owner, 170 | repo: context.repo.repo, 171 | issue_number: issueNumber, 172 | per_page: 100, 173 | headers: { 174 | accept: 'application/vnd.github.mockingbird-preview+json' 175 | } 176 | }) 177 | 178 | console.log(`Timeline: ${JSON.stringify(timeline, null, 2)}`) 179 | 180 | const linkedPRs = timeline 181 | // filter out event.event is not cross-referenced 182 | .filter(event => event.event === 'cross-referenced') 183 | // filter out event.source?.issue?.pull_request is null 184 | .filter(event => event.source?.issue?.pull_request?.url) 185 | // return url and pr name, and the issue number and the body and the actor 186 | .map(event => ({ 187 | issueNumber: event.source?.issue?.number, 188 | actor: event.actor?.login, 189 | url: event.source?.issue?.pull_request?.url, 190 | title: event.source?.issue?.title, 191 | body: event.source?.issue?.body, 192 | })) 193 | 194 | hasLinkedPR = linkedPRs.length > 0 195 | console.log(`Linked PRs:`, linkedPRs) 196 | console.log(`Issue Comment #${issueNumber}, already has linked PR: ${hasLinkedPR}`) 197 | 198 | } catch (error) { 199 | console.error('Error checking for linked PRs:', error) 200 | } 201 | } 202 | 203 | } else if (context.eventName === 'pull_request_review_comment' || context.eventName === 'pull_request_review') { 204 | // PR review related 205 | isPR = true 206 | issueNumber = context.payload.pull_request.number 207 | prNumber = context.payload.pull_request.number 208 | headRef = context.payload.pull_request.head.ref 209 | baseRef = context.payload.pull_request.base.ref 210 | headSha = context.payload.pull_request.head.sha 211 | 212 | if (context.eventName === 'pull_request_review_comment') { 213 | triggerText = context.payload.comment.body 214 | } else { 215 | triggerText = context.payload.review.body 216 | } 217 | 218 | console.log(`PR Review #${prNumber}: ${baseRef} <- ${headRef}`) 219 | } 220 | 221 | // Set outputs 222 | core.setOutput('issue-number', issueNumber) 223 | core.setOutput('pr-number', prNumber || '') 224 | core.setOutput('head-ref', headRef || '') 225 | core.setOutput('base-ref', baseRef || '') 226 | core.setOutput('head-sha', headSha || '') 227 | core.setOutput('is-pr', isPR) 228 | core.setOutput('trigger-text', triggerText) 229 | core.setOutput('has-linked-pr', hasLinkedPR) 230 | 231 | console.log(`Final Context:`) 232 | console.log(`Event: ${context.eventName}`) 233 | console.log(`Issue #${issueNumber}`) 234 | console.log(`isPR: ${isPR}`) 235 | console.log(`Trigger Text: ${triggerText}`) 236 | console.log(`Already has linked PR: ${hasLinkedPR}`) 237 | 238 | - name: Validate Environment 239 | run: | 240 | echo "🔍 Runtime Environment Information" 241 | echo "==================================" 242 | echo "Event: ${{ github.event_name }}" 243 | echo "Actor: ${{ github.actor }}" 244 | echo "Repository: ${{ github.repository }}" 245 | echo "Issue Number: ${{ steps.context-info.outputs.issue-number }}" 246 | echo "Is PR: ${{ steps.context-info.outputs.is-pr }}" 247 | echo "PR Number: ${{ steps.context-info.outputs.pr-number }}" 248 | echo "Head Ref: ${{ steps.context-info.outputs.head-ref }}" 249 | echo "Base Ref: ${{ steps.context-info.outputs.base-ref }}" 250 | echo "Head SHA: ${{ steps.context-info.outputs.head-sha }}" 251 | echo "Has Linked PR: ${{ steps.context-info.outputs.has-linked-pr }}" 252 | echo "==================================" 253 | 254 | # Check for secrets 255 | if [ -z "${{ secrets.CLAUDE_CREDS_API_KEY }}" ]; then 256 | echo "::error::CLAUDE_CREDS_API_KEY is not set" 257 | exit 1 258 | fi 259 | 260 | if [ -z "${{ secrets.CLAUDE_CREDS_API }}" ]; then 261 | echo "::error::CLAUDE_CREDS_API is not set" 262 | exit 1 263 | fi 264 | 265 | echo "✅ Environment validation complete" 266 | 267 | - name: Exit early if Issue already has linked PR 268 | id: should-continue 269 | run: | 270 | IS_PR="${{ steps.context-info.outputs.is-pr }}" 271 | HAS_LINKED_PR="${{ steps.context-info.outputs.has-linked-pr }}" 272 | 273 | if [[ "$IS_PR" == "false" && "$HAS_LINKED_PR" == "true" ]]; then 274 | echo "Issue already has linked PR. Will skip remaining steps." 275 | echo "should-continue=false" >> $GITHUB_OUTPUT 276 | else 277 | echo "No linked PRs found or this is a PR. Continuing." 278 | echo "should-continue=true" >> $GITHUB_OUTPUT 279 | fi 280 | 281 | - name: Debug issue number 282 | run: echo "The issue number is ${{ steps.context-info.outputs.issue-number }}" 283 | 284 | # Only add comment if it doesn't exist 285 | - name: Find existing status comment 286 | if: steps.should-continue.outputs.should-continue == 'true' 287 | uses: peter-evans/find-comment@v3 288 | id: find_comment # We'll check the output of this step 289 | with: 290 | issue-number: ${{ steps.context-info.outputs.issue-number }} 291 | comment-author: 'github-actions[bot]' 292 | body-includes: '' 293 | 294 | - name: Create initial "in-progress" comment if it doesn't exist 295 | # This step ONLY runs if the 'find-comment' step found nothing 296 | if: steps.should-continue.outputs.should-continue == 'true' && steps.find_comment.outputs.comment-id == '' 297 | uses: peter-evans/create-or-update-comment@v4 298 | id: create_comment 299 | with: 300 | issue-number: ${{ steps.context-info.outputs.issue-number }} 301 | body: | 302 | Claude Code is running... ⏳ 303 | 304 | ######################################################### 305 | # Claude Code 306 | ######################################################### 307 | claude: 308 | needs: setup 309 | # Security-focused conditional execution (full support for Issues and PRs) 310 | if: needs.setup.outputs.should-continue == 'true' 311 | runs-on: ubuntu-latest 312 | timeout-minutes: 20 313 | permissions: 314 | # 📁 Content management (highest permissions) 315 | contents: write 316 | pull-requests: write 317 | issues: write 318 | discussions: write 319 | 320 | # 🔧 Development & CI/CD management 321 | actions: write 322 | checks: write 323 | statuses: write 324 | pages: write 325 | deployments: write 326 | 327 | # 📦 Package & security management 328 | packages: write 329 | security-events: write 330 | 331 | # 🎯 Project management 332 | repository-projects: write 333 | 334 | # 🆔 Authentication & token management 335 | id-token: write 336 | 337 | steps: 338 | - name: Checkout Repository 339 | uses: actions/checkout@v4 340 | with: 341 | # Checkout the feature branch for PRs, or the default branch for Issues 342 | ref: ${{ needs.setup.outputs.head-sha || github.ref }} 343 | fetch-depth: ${{ needs.setup.outputs.is-pr == 'true' && 0 || 1 }} 344 | token: ${{ secrets.GITHUB_TOKEN }} 345 | 346 | - name: Validate Environment 347 | run: | 348 | echo "🔍 Runtime Environment Information" 349 | echo "==================================" 350 | echo "Event: ${{ github.event_name }}" 351 | echo "Actor: ${{ github.actor }}" 352 | echo "Repository: ${{ github.repository }}" 353 | echo "Issue Number: ${{ needs.setup.outputs.issue-number }}" 354 | echo "Is PR: ${{ needs.setup.outputs.is-pr }}" 355 | echo "PR Number: ${{ needs.setup.outputs.pr-number }}" 356 | echo "Head Ref: ${{ needs.setup.outputs.head-ref }}" 357 | echo "Base Ref: ${{ needs.setup.outputs.base-ref }}" 358 | echo "Head SHA: ${{ needs.setup.outputs.head-sha }}" 359 | echo "Has Linked PR: ${{ needs.setup.outputs.has-linked-pr }}" 360 | echo "==================================" 361 | 362 | - name: Fetch Base Branch (PR only) 363 | if: needs.setup.outputs.is-pr == 'true' && needs.setup.outputs.base-ref 364 | run: | 365 | echo "📥 Fetching base branch: ${{ needs.setup.outputs.base-ref }}" 366 | git fetch origin ${{ needs.setup.outputs.base-ref }}:${{ needs.setup.outputs.base-ref }} 367 | 368 | echo "📋 Changed files:" 369 | git diff --name-only origin/${{ needs.setup.outputs.base-ref }}..HEAD || echo "Failed to get diff" 370 | 371 | echo "📊 Change statistics:" 372 | git diff --stat origin/${{ needs.setup.outputs.base-ref }}..HEAD || echo "Failed to get stats" 373 | 374 | - name: Get Project Information 375 | id: project-info 376 | run: | 377 | echo "📁 Collecting project information" 378 | 379 | # Determine project type 380 | project_type="unknown" 381 | framework="" 382 | 383 | if [ -f "package.json" ]; then 384 | project_type="node" 385 | echo "📦 Node.js project detected" 386 | 387 | # Detect framework 388 | if grep -q "next" package.json; then 389 | framework="Next.js" 390 | elif grep -q "react" package.json; then 391 | framework="React" 392 | elif grep -q "vue" package.json; then 393 | framework="Vue.js" 394 | elif grep -q "angular" package.json; then 395 | framework="Angular" 396 | elif grep -q "express" package.json; then 397 | framework="Express" 398 | fi 399 | elif [ -f "requirements.txt" ] || [ -f "pyproject.toml" ]; then 400 | project_type="python" 401 | framework="Python" 402 | echo "🐍 Python project detected" 403 | elif [ -f "Cargo.toml" ]; then 404 | project_type="rust" 405 | framework="Rust" 406 | echo "🦀 Rust project detected" 407 | elif [ -f "go.mod" ]; then 408 | project_type="go" 409 | framework="Go" 410 | echo "🐹 Go project detected" 411 | elif [ -f "pom.xml" ] || [ -f "build.gradle" ]; then 412 | project_type="java" 413 | framework="Java" 414 | echo "☕ Java project detected" 415 | fi 416 | 417 | echo "project-type=$project_type" >> $GITHUB_OUTPUT 418 | echo "framework=$framework" >> $GITHUB_OUTPUT 419 | 420 | # Estimate number of files 421 | total_files=$(find . -type f \( -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" -o -name "*.py" -o -name "*.rs" -o -name "*.go" -o -name "*.java" \) | wc -l) 422 | echo "total-files=$total_files" >> $GITHUB_OUTPUT 423 | 424 | echo "📊 Project summary: $framework ($total_files files)" 425 | 426 | - name: Setup Env 427 | id: setup-env 428 | uses: DavidWells/actions/get-claude-tokens@master 429 | with: 430 | api-key: ${{ secrets.CLAUDE_CREDS_API_KEY }} 431 | api-endpoint: ${{ secrets.CLAUDE_CREDS_API }} 432 | 433 | # - name: Run Claude PR Action 434 | # uses: davidwells/claude-code-action@main 435 | # with: 436 | # use_oauth: true 437 | # claude_access_token: ${{ steps.setup-env.outputs.access-token }} 438 | # claude_refresh_token: ${{ steps.setup-env.outputs.refresh-token }} 439 | # claude_expires_at: ${{ steps.setup-env.outputs.expires-at }} 440 | # model: ${{ steps.setup-env.outputs.model || 'claude-sonnet-4-20250514' }} 441 | # allowed_tools: ${{ steps.setup-env.outputs.allowed_tools || 'Bash,Edit,Read,Write,Glob,Grep,LS,MultiEdit,NotebookRead,NotebookEdit' }} 442 | # timeout_minutes: "60" 443 | 444 | - name: Run Claude Code 445 | id: claude 446 | uses: DavidWells/claude-code-action@main 447 | timeout-minutes: 20 448 | continue-on-error: true 449 | with: 450 | claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} 451 | model: ${{ steps.setup-env.outputs.model || 'claude-sonnet-4-20250514' }} 452 | # This is an optional setting that allows Claude to read CI results on PRs 453 | additional_permissions: | 454 | actions: read 455 | # claude_access_token: ${{ secrets.CLAUDE_ACCESS_TOKEN }} 456 | # claude_refresh_token: ${{ secrets.CLAUDE_REFRESH_TOKEN }} 457 | # claude_expires_at: ${{ secrets.CLAUDE_EXPIRES_AT }} 458 | # GITHUB ACTIONS (Maximum Freedom): 459 | allowed_tools: | 460 | Edit,View,Replace,Write,Create, 461 | BatchTool,GlobTool,GrepTool,NotebookEditCell, 462 | Bash(git:*),Bash(npm:*),Bash(yarn:*),Bash(python:*), 463 | Bash(docker:*),Bash(make:*),Bash(cargo:*),Bash(go:*), 464 | Bash(ls:*),Bash(cat:*),Bash(echo:*),Bash(curl:*), 465 | mcp__* 466 | disallowed_tools: | 467 | Bash(sudo:*), 468 | Bash(rm -rf /) 469 | env: 470 | # Pass context information to Claude Code 471 | GITHUB_CONTEXT_TYPE: ${{ needs.setup.outputs.is-pr == 'true' && 'PR' || 'ISSUE' }} 472 | ISSUE_NUMBER: ${{ needs.setup.outputs.issue-number }} 473 | PR_NUMBER: ${{ needs.setup.outputs.pr-number }} 474 | BASE_BRANCH: ${{ needs.setup.outputs.base-ref }} 475 | HEAD_BRANCH: ${{ needs.setup.outputs.head-ref }} 476 | HEAD_SHA: ${{ needs.setup.outputs.head-sha }} 477 | GITHUB_EVENT_NAME: ${{ github.event_name }} 478 | TRIGGER_TEXT: ${{ needs.setup.outputs.trigger-text }} 479 | PROJECT_TYPE: ${{ steps.project-info.outputs.project-type }} 480 | PROJECT_FRAMEWORK: ${{ steps.project-info.outputs.framework }} 481 | TOTAL_FILES: ${{ steps.project-info.outputs.total-files }} 482 | GITHUB_ACTOR: ${{ github.actor }} 483 | REPOSITORY_NAME: ${{ github.repository }} 484 | 485 | # 🔑 Enhanced permission information 486 | CLAUDE_PERMISSIONS_LEVEL: "ENHANCED" 487 | REPO_ADMIN_MODE: "true" 488 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 489 | 490 | # 📊 Repository information 491 | REPOSITORY_OWNER: ${{ github.repository_owner }} 492 | REPOSITORY_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} 493 | REPOSITORY_PRIVATE: ${{ github.event.repository.private }} 494 | REPOSITORY_FORK: ${{ github.event.repository.fork }} 495 | 496 | # 🎯 Execution context 497 | WORKFLOW_RUN_ID: ${{ github.run_id }} 498 | WORKFLOW_RUN_NUMBER: ${{ github.run_number }} 499 | COMMIT_SHA: ${{ github.sha }} 500 | REF_NAME: ${{ github.ref_name }} 501 | 502 | # 🔧 Available feature flags 503 | CAN_CREATE_RELEASES: "true" 504 | CAN_MANAGE_LABELS: "true" 505 | CAN_MANAGE_MILESTONES: "true" 506 | CAN_MANAGE_PROJECTS: "true" 507 | CAN_MANAGE_WIKI: "true" 508 | CAN_MANAGE_PAGES: "true" 509 | CAN_MANAGE_DEPLOYMENTS: "true" 510 | CAN_MANAGE_SECURITY: "true" 511 | CAN_MANAGE_PACKAGES: "true" 512 | CAN_MANAGE_ACTIONS: "true" 513 | 514 | - name: Run Advanced Repository Management 515 | id: advanced-management 516 | if: steps.claude.outcome == 'success' && needs.setup.outputs.issue-number 517 | uses: actions/github-script@v7 518 | with: 519 | github-token: ${{ secrets.GITHUB_TOKEN }} 520 | script: | 521 | const issueNumber = ${{ needs.setup.outputs.issue-number }}; 522 | const isPR = '${{ needs.setup.outputs.is-pr }}' === 'true'; 523 | const triggerText = (${{ toJSON(needs.setup.outputs.trigger-text) }} || '').toLowerCase(); 524 | const framework = '${{ steps.project-info.outputs.framework }}'; 525 | const hashSymbol = String.fromCharCode(35); 526 | 527 | console.log('🚀 Starting advanced repository management...'); 528 | 529 | const managementResults = { 530 | labels: [], 531 | milestones: [], 532 | projects: [], 533 | releases: [], 534 | security: [], 535 | wiki: [], 536 | pages: [], 537 | actions: [] 538 | }; 539 | 540 | try { 541 | // 1. 🏷️ Intelligent Label Management 542 | console.log('📋 Running automatic label management...'); 543 | 544 | // Automatically create necessary labels 545 | const requiredLabels = [ 546 | { name: 'claude-code', color: '7B68EE', description: 'Items created or modified by Claude Code' }, 547 | { name: 'auto-generated', color: '00D084', description: 'Automatically generated content' }, 548 | { name: 'security-fix', color: 'FF4444', description: 'Security-related fixes' }, 549 | { name: 'performance', color: 'FFA500', description: 'Performance improvements' }, 550 | { name: 'technical-debt', color: '8B4513', description: 'Resolving technical debt' }, 551 | { name: 'documentation', color: '0366D6', description: 'Documentation related' }, 552 | { name: 'ci-cd', color: '28A745', description: 'CI/CD improvements' } 553 | ]; 554 | 555 | for (const label of requiredLabels) { 556 | try { 557 | await github.rest.issues.createLabel({ 558 | owner: context.repo.owner, 559 | repo: context.repo.repo, 560 | name: label.name, 561 | color: label.color, 562 | description: label.description 563 | }); 564 | managementResults.labels.push(`✅ Created: ${label.name}`); 565 | } catch (error) { 566 | if (error.status === 422) { 567 | managementResults.labels.push(`📋 Exists: ${label.name}`); 568 | } else { 569 | managementResults.labels.push(`❌ Error: ${label.name} - ${error.message}`); 570 | } 571 | } 572 | } 573 | 574 | // Automatically apply relevant labels 575 | const autoLabels = ['claude-code', 'auto-generated']; 576 | if (triggerText.includes('security')) { 577 | autoLabels.push('security-fix'); 578 | } 579 | if (triggerText.includes('performance')) { 580 | autoLabels.push('performance'); 581 | } 582 | if (triggerText.includes('technical debt')) { 583 | autoLabels.push('technical-debt'); 584 | } 585 | if (triggerText.includes('document')) { 586 | autoLabels.push('documentation'); 587 | } 588 | if (triggerText.includes('ci') || triggerText.includes('cd') || triggerText.includes('deploy')) { 589 | autoLabels.push('ci-cd'); 590 | } 591 | 592 | await github.rest.issues.addLabels({ 593 | owner: context.repo.owner, 594 | repo: context.repo.repo, 595 | issue_number: issueNumber, 596 | labels: autoLabels 597 | }); 598 | 599 | managementResults.labels.push(`🏷️ Applied: ${autoLabels.join(', ')}`); 600 | 601 | // 2. 🎯 Automatic Milestone Management 602 | console.log('🎯 Running milestone management...'); 603 | 604 | try { 605 | // Create a milestone for the current year and month 606 | const now = new Date(); 607 | const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; 608 | const currentMilestone = `${now.getFullYear()}-${monthNames[now.getMonth()]}`; 609 | 610 | try { 611 | const milestone = await github.rest.issues.createMilestone({ 612 | owner: context.repo.owner, 613 | repo: context.repo.repo, 614 | title: currentMilestone, 615 | description: `Tasks and improvements for ${currentMilestone}`, 616 | due_on: new Date(now.getFullYear(), now.getMonth() + 1, 0).toISOString() 617 | }); 618 | managementResults.milestones.push(`✅ Created: ${currentMilestone}`); 619 | } catch (error) { 620 | if (error.status === 422) { 621 | managementResults.milestones.push(`📅 Exists: ${currentMilestone}`); 622 | } else { 623 | managementResults.milestones.push(`❌ Error: ${error.message}`); 624 | } 625 | } 626 | } catch (error) { 627 | managementResults.milestones.push(`❌ Milestone management error: ${error.message}`); 628 | } 629 | 630 | // 3. 📊 Project Board Management 631 | console.log('📊 Running project management...'); 632 | 633 | try { 634 | // Get projects for the repository 635 | const projects = await github.rest.projects.listForRepo({ 636 | owner: context.repo.owner, 637 | repo: context.repo.repo 638 | }); 639 | 640 | if (projects.data.length > 0) { 641 | const project = projects.data[0]; 642 | managementResults.projects.push(`📊 Project detected: ${project.name}`); 643 | 644 | // Add a card to the To Do column 645 | const columns = await github.rest.projects.listColumns({ 646 | project_id: project.id 647 | }); 648 | 649 | const todoColumn = columns.data.find(col => 650 | col.name.toLowerCase().includes('todo') || 651 | col.name.toLowerCase().includes('backlog') 652 | ); 653 | 654 | if (todoColumn) { 655 | await github.rest.projects.createCard({ 656 | column_id: todoColumn.id, 657 | content_id: context.payload.issue.id, // Use issue ID for content_id 658 | content_type: 'Issue' 659 | }); 660 | managementResults.projects.push(`📋 Card added: ${project.name}`); 661 | } 662 | } else { 663 | managementResults.projects.push(`ℹ️ Project board not found`); 664 | } 665 | } catch (error) { 666 | managementResults.projects.push(`❌ Project management error: ${error.message}`); 667 | } 668 | 669 | // 4. 🔒 Security Alert Handling 670 | console.log('🔒 Running security check...'); 671 | 672 | try { 673 | if (triggerText.includes('security') || triggerText.includes('vulnerability')) { 674 | // Check for security alerts 675 | try { 676 | const vulnerabilities = await github.rest.secretScanning.listAlertsForRepo({ 677 | owner: context.repo.owner, 678 | repo: context.repo.repo, 679 | state: 'open' 680 | }); 681 | 682 | managementResults.security.push(`🔍 Open security alerts: ${vulnerabilities.data.length}`); 683 | 684 | if (vulnerabilities.data.length > 0) { 685 | managementResults.security.push(`⚠️ Security alert confirmation required`); 686 | } 687 | } catch (error) { 688 | managementResults.security.push(`ℹ️ Security alert check: Access restricted or feature disabled`); 689 | } 690 | } else { 691 | managementResults.security.push(`ℹ️ Security check: Skipped`); 692 | } 693 | } catch (error) { 694 | managementResults.security.push(`❌ Security check error: ${error.message}`); 695 | } 696 | 697 | // 5. 📚 Automatic Wiki Update 698 | console.log('📚 Running Wiki management...'); 699 | 700 | try { 701 | if (triggerText.includes('wiki') || triggerText.includes('document')) { 702 | // Check if Wiki page exists 703 | try { 704 | const repoInfo = await github.rest.repos.get({ 705 | owner: context.repo.owner, 706 | repo: context.repo.repo 707 | }); 708 | 709 | if (repoInfo.data.has_wiki) { 710 | managementResults.wiki.push(`📚 Wiki enabled: Updatable`); 711 | // Actual Wiki update is performed by Claude Code 712 | } else { 713 | managementResults.wiki.push(`📚 Wiki disabled: Needs to be enabled in settings`); 714 | } 715 | } catch (error) { 716 | managementResults.wiki.push(`❌ Wiki check error: ${error.message}`); 717 | } 718 | } else { 719 | managementResults.wiki.push(`ℹ️ Wiki update: Skipped`); 720 | } 721 | } catch (error) { 722 | managementResults.wiki.push(`❌ Wiki management error: ${error.message}`); 723 | } 724 | 725 | // 6. 🌐 GitHub Pages Management 726 | console.log('🌐 Running GitHub Pages management...'); 727 | 728 | try { 729 | if (triggerText.includes('pages') || triggerText.includes('deploy') || triggerText.includes('site')) { 730 | try { 731 | const pages = await github.rest.repos.getPages({ 732 | owner: context.repo.owner, 733 | repo: context.repo.repo 734 | }); 735 | 736 | managementResults.pages.push(`🌐 Pages enabled: ${pages.data.html_url}`); 737 | 738 | // Trigger a Pages build 739 | await github.rest.repos.requestPagesBuild({ 740 | owner: context.repo.owner, 741 | repo: context.repo.repo 742 | }); 743 | 744 | managementResults.pages.push(`🔄 Triggered Pages rebuild`); 745 | } catch (error) { 746 | if (error.status === 404) { 747 | managementResults.pages.push(`🌐 Pages disabled: Needs to be enabled in settings`); 748 | } else { 749 | managementResults.pages.push(`❌ Pages management error: ${error.message}`); 750 | } 751 | } 752 | } else { 753 | managementResults.pages.push(`ℹ️ Pages management: Skipped`); 754 | } 755 | } catch (error) { 756 | managementResults.pages.push(`❌ Pages management error: ${error.message}`); 757 | } 758 | 759 | // 7. ⚙️ Actions Workflow Management 760 | console.log('⚙️ Running Actions management...'); 761 | 762 | try { 763 | if (triggerText.includes('workflow') || triggerText.includes('action') || triggerText.includes('ci') || triggerText.includes('cd')) { 764 | const workflows = await github.rest.actions.listRepoWorkflows({ 765 | owner: context.repo.owner, 766 | repo: context.repo.repo 767 | }); 768 | 769 | managementResults.actions.push(`⚙️ Number of workflows: ${workflows.data.total_count}`); 770 | 771 | // Check for disabled workflows 772 | const disabledWorkflows = workflows.data.workflows.filter(w => w.state === 'disabled_manually'); 773 | if (disabledWorkflows.length > 0) { 774 | managementResults.actions.push(`⚠️ Disabled workflows: ${disabledWorkflows.length}`); 775 | } 776 | } else { 777 | managementResults.actions.push(`ℹ️ Actions management: Skipped`); 778 | } 779 | } catch (error) { 780 | managementResults.actions.push(`❌ Actions management error: ${error.message}`); 781 | } 782 | 783 | console.log('✅ Advanced repository management complete'); 784 | 785 | // Save results to output 786 | core.setOutput('management-results', JSON.stringify(managementResults)); 787 | core.setOutput('management-success', 'true'); 788 | 789 | } catch (error) { 790 | console.error('❌ Advanced repository management error:', error); 791 | core.setOutput('management-error', error.message); 792 | core.setOutput('management-success', 'false'); 793 | } 794 | 795 | - name: Check for Changes and Prepare for PR 796 | id: check-changes 797 | if: steps.claude.outcome == 'success' && needs.setup.outputs.is-pr == 'false' && steps.claude.outputs.branch_name 798 | run: | 799 | set -e # Exit immediately if a command exits with a non-zero status. 800 | 801 | BRANCH_NAME="${{ steps.claude.outputs.branch_name }}" 802 | DEFAULT_BRANCH="origin/${{ github.event.repository.default_branch }}" 803 | 804 | echo "--- 1. Checking if remote branch '${BRANCH_NAME}' exists ---" 805 | # Use `git ls-remote` to check for the branch's existence. It exits with 0 if it exists, 2 if not. 806 | if ! git ls-remote --exit-code --heads origin "${BRANCH_NAME}" >/dev/null 2>&1; then 807 | echo "✅ Remote branch '${BRANCH_NAME}' not found. This indicates no code changes were committed." 808 | echo "has-changes=false" >> $GITHUB_OUTPUT 809 | echo "branch-exists=false" >> $GITHUB_OUTPUT 810 | # Exit successfully as this is an expected outcome. 811 | exit 0 812 | fi 813 | 814 | echo "✅ Remote branch found. Proceeding with original fetch and reset logic." 815 | echo "branch-exists=true" >> $GITHUB_OUTPUT 816 | 817 | echo "--- 2. DEBUG: Initial Git State ---" 818 | echo "Current branch: $(git rev-parse --abbrev-ref HEAD)" 819 | echo "Current commit: $(git log -1 --pretty=%h)" 820 | echo "Workspace status:" 821 | git status -s 822 | echo "-----------------------------------" 823 | 824 | echo "🚀 3. Fetching the specific branch pushed by Claude: '${BRANCH_NAME}'..." 825 | git fetch origin "+refs/heads/${BRANCH_NAME}:refs/remotes/origin/${BRANCH_NAME}" 826 | 827 | echo "--- 4. DEBUG: After Fetch ---" 828 | echo "Remote commit for '${BRANCH_NAME}' is: $(git log origin/${BRANCH_NAME} -1 --pretty=%h)" 829 | echo "-----------------------------" 830 | 831 | echo "🔄 5. Forcibly resetting local branch to match the fetched remote state..." 832 | git checkout "${BRANCH_NAME}" 833 | git reset --hard "origin/${BRANCH_NAME}" 834 | 835 | echo "--- 6. DEBUG: After Resetting Local Branch ---" 836 | echo "Current branch is now: $(git rev-parse --abbrev-ref HEAD)" 837 | echo "Current commit is now: $(git log -1 --pretty=%h)" 838 | echo "Workspace status is now:" 839 | git status -s 840 | echo "---------------------------------------------" 841 | 842 | BRANCH_RANGE="${DEFAULT_BRANCH}...${BRANCH_NAME}" 843 | 844 | echo "🔍 7. Checking for changes in range: ${BRANCH_RANGE}..." 845 | 846 | # Use the exit code of 'git diff --quiet' to check for changes. 847 | if git diff --quiet $BRANCH_RANGE; then 848 | echo "✅ No changes detected between branches. Setting has-changes=false." 849 | echo "has-changes=false" >> $GITHUB_OUTPUT 850 | else 851 | echo "📝 Changes detected. Setting has-changes=true." 852 | echo "has-changes=true" >> $GITHUB_OUTPUT 853 | 854 | echo "---" 855 | echo "📄 Changed files (compared to default branch):" 856 | git diff --name-only $BRANCH_RANGE 857 | 858 | echo "---" 859 | echo "📊 Change statistics:" 860 | git diff --stat $BRANCH_RANGE 861 | fi 862 | ######################################################### 863 | # IF we have changes, create or update a pull request 864 | ######################################################### 865 | - name: Create or Update Pull Request 866 | id: auto-pr 867 | # The 'if' condition is now correctly chained. 868 | if: | 869 | steps.claude.outcome == 'success' 870 | && needs.setup.outputs.is-pr == 'false' 871 | && steps.claude.outputs.branch_name 872 | && steps.check-changes.outputs.has-changes == 'true' 873 | uses: actions/github-script@v7 874 | with: 875 | github-token: ${{ secrets.GITHUB_TOKEN }} 876 | script: | 877 | const issueNumber = ${{ needs.setup.outputs.issue-number }} 878 | const branchName = '${{ steps.claude.outputs.branch_name }}' 879 | const defaultBranch = '${{ github.event.repository.default_branch }}' 880 | const owner = context.repo.owner 881 | const repo = context.repo.repo 882 | 883 | try { 884 | // 1. Check for an existing PR for this branch 885 | console.log(`Checking for existing PRs for branch: ${branchName}`) 886 | 887 | let existingPr = null 888 | try { 889 | const { data: pulls } = await github.rest.pulls.list({ 890 | owner, 891 | repo, 892 | head: `${owner}:${branchName}`, 893 | state: 'open', 894 | per_page: 1 895 | }) 896 | existingPr = pulls.length > 0 ? pulls[0] : null 897 | } catch (error) { 898 | existingPr = null 899 | } 900 | 901 | let pr 902 | 903 | if (existingPr) { 904 | // 2. If PR exists, use it 905 | pr = existingPr 906 | console.log(`✅ Found existing PR: #${pr.number}. No new PR will be created.`) 907 | 908 | // Optional: Post a comment to the existing PR to notify of the update 909 | const updateComment = `🔄 **Claude Code has updated this branch** with new changes.\n\n[View the latest workflow run](https://github.com/${owner}/${repo}/actions/runs/${{ github.run_id }})` 910 | await github.rest.issues.createComment({ 911 | owner, 912 | repo, 913 | issue_number: pr.number, 914 | body: updateComment, 915 | }) 916 | 917 | } else { 918 | // 3. If no PR exists, create one 919 | console.log(`No existing PR found. Attempting to create a new PR for branch: ${branchName}`) 920 | 921 | const { data: issue } = await github.rest.issues.get({ 922 | owner, 923 | repo, 924 | issue_number: issueNumber 925 | }) 926 | 927 | // Get the last issue comment by claude on issueNumber 928 | const { data: comments } = await github.rest.issues.listComments({ 929 | owner, 930 | repo, 931 | issue_number: issueNumber 932 | }) 933 | const claudeComment = comments.find(c => c.user.login === 'claude[bot]' && c.body.includes('Claude')) 934 | const claudeCommentBody = claudeComment ? claudeComment.body.replace(/\[Create PR ➔\]\([^)]+\)/, '') : '' 935 | 936 | const prTitle = `🤖 Claude Code: ${issue.title}` 937 | const prBody = `## 🤖 Automated fix by Claude Code 938 | 939 | **Related Issue:** #${issueNumber} 940 | **Executed by:** @${{ github.actor }} 941 | 942 | --- 943 | 944 | ${claudeCommentBody} 945 | 946 | **If additional fixes are needed:** Mention \`@ claude\` in a comment on this PR. 947 | 948 | *resolves #${issueNumber}* 949 | 950 | --- 951 | *Automated PR by [Claude Code Action - Run #${{ github.run_id }}](https://github.com/${owner}/${repo}/actions/runs/${{ github.run_id }})* 952 | ` 953 | 954 | const { data: newPr } = await github.rest.pulls.create({ 955 | owner, 956 | repo, 957 | title: prTitle, 958 | head: branchName, 959 | base: defaultBranch, 960 | body: prBody, 961 | draft: false 962 | }) 963 | 964 | pr = newPr 965 | console.log(`🎉 PR created successfully: #${pr.number}`) 966 | 967 | // Add first comment to the PR for sticky comment 968 | const stickyComment = ` 969 | **ℹ️ Action Run:** https://github.com/${owner}/${repo}/actions/runs/${{ github.run_id }} 970 | 971 | 972 | ` 973 | 974 | /* Add sticky comment to the PR for other data */ 975 | await github.rest.issues.createComment({ 976 | issue_number: pr.number, 977 | owner, 978 | repo, 979 | body: stickyComment, 980 | author: 'github-actions[bot]' 981 | }) 982 | } 983 | 984 | // 4. Set outputs with the PR info (whether new or existing) 985 | core.setOutput('pr-number', pr.number) 986 | core.setOutput('pr-url', pr.html_url) 987 | core.setOutput('branch-name', branchName) 988 | 989 | // Ping our webhook - PR creation successful 990 | try { 991 | const webhookUrl = '${{ secrets.CLAUDE_CODE_NOTIFICATIONS_URL }}'; 992 | const webhookToken = '${{ secrets.CLAUDE_CODE_NOTIFICATIONS_KEY }}'; 993 | 994 | const jobId = `claude-${context.repo.owner}-${context.repo.repo}-${issueNumber}-${Date.now()}`; 995 | const actionUrl = `https://github.com/${owner}/${repo}/issues/${issueNumber}`; 996 | 997 | const webhookData = { 998 | jobId: jobId, 999 | status: 'completed', 1000 | repository: `${context.repo.owner}/${context.repo.repo}`, 1001 | url: actionUrl, 1002 | branch: branchName || context.payload.repository?.default_branch || 'main', 1003 | commit: '${{ github.sha }}', 1004 | results: { 1005 | prCreated: true, 1006 | prNumber: pr.number, 1007 | prUrl: pr.html_url, 1008 | issueNumber: issueNumber, 1009 | actor: '${{ github.actor }}', 1010 | event: '${{ github.event_name }}', 1011 | framework: '${{ steps.project-info.outputs.framework }}', 1012 | totalFiles: '${{ steps.project-info.outputs.total-files }}', 1013 | hasChanges: '${{ steps.check-changes.outputs.has-changes }}', 1014 | managementSuccess: '${{ steps.advanced-management.outputs.management-success }}' 1015 | } 1016 | }; 1017 | 1018 | console.log('🔔 Sending webhook notification (PR created)...'); 1019 | console.log('Webhook URL:', webhookUrl); 1020 | console.log('Job ID:', jobId); 1021 | 1022 | const response = await fetch(`${webhookUrl}/job-complete`, { 1023 | method: 'POST', 1024 | headers: { 1025 | 'Content-Type': 'application/json', 1026 | 'Authorization': `Bearer ${webhookToken}` 1027 | }, 1028 | body: JSON.stringify(webhookData) 1029 | }); 1030 | 1031 | if (response.ok) { 1032 | console.log('✅ Webhook notification sent successfully'); 1033 | } else { 1034 | console.warn(`⚠️ Webhook notification failed: ${response.status} ${response.statusText}`); 1035 | } 1036 | } catch (error) { 1037 | console.warn('⚠️ Webhook notification error:', error.message); 1038 | } 1039 | 1040 | } catch (error) { 1041 | console.error('❌ PR creation/update error:', error) 1042 | core.setOutput('error', error.message) 1043 | 1044 | const failureComment = `❌ **Automatic PR creation failed**\n\n**Error:** \`${error.message}\`\n\n**Solution**: A branch named \`${branchName}\` was created with the changes. Please create a PR from it manually or rerun the job.\n**Details**: [Actions run log](${{ github.server_url }}/${owner}/${repo}/actions/runs/${{ github.run_id }})` 1045 | await github.rest.issues.createComment({ 1046 | issue_number: issueNumber, 1047 | owner, 1048 | repo, 1049 | body: failureComment 1050 | }) 1051 | 1052 | // Ping our webhook - PR creation failed 1053 | try { 1054 | const webhookUrl = '${{ secrets.CLAUDE_CODE_NOTIFICATIONS_URL }}'; 1055 | const webhookToken = '${{ secrets.CLAUDE_CODE_NOTIFICATIONS_KEY }}'; 1056 | 1057 | const jobId = `claude-${context.repo.owner}-${context.repo.repo}-${issueNumber}-${Date.now()}`; 1058 | const actionUrl = `https://github.com/${owner}/${repo}/issues/${issueNumber}`; 1059 | 1060 | const webhookData = { 1061 | jobId: jobId, 1062 | status: 'failed', 1063 | repository: `${context.repo.owner}/${context.repo.repo}`, 1064 | url: actionUrl, 1065 | branch: branchName || context.payload.repository?.default_branch || 'main', 1066 | commit: '${{ github.sha }}', 1067 | results: { 1068 | prCreated: false, 1069 | prNumber: null, 1070 | prUrl: null, 1071 | issueNumber: issueNumber, 1072 | actor: '${{ github.actor }}', 1073 | event: '${{ github.event_name }}', 1074 | framework: '${{ steps.project-info.outputs.framework }}', 1075 | totalFiles: '${{ steps.project-info.outputs.total-files }}', 1076 | hasChanges: '${{ steps.check-changes.outputs.has-changes }}', 1077 | managementSuccess: '${{ steps.advanced-management.outputs.management-success }}', 1078 | error: error.message 1079 | } 1080 | }; 1081 | 1082 | console.log('🔔 Sending webhook notification (PR creation failed)...'); 1083 | console.log('Webhook URL:', webhookUrl); 1084 | console.log('Job ID:', jobId); 1085 | 1086 | const response = await fetch(`${webhookUrl}/job-complete`, { 1087 | method: 'POST', 1088 | headers: { 1089 | 'Content-Type': 'application/json', 1090 | 'Authorization': `Bearer ${webhookToken}` 1091 | }, 1092 | body: JSON.stringify(webhookData) 1093 | }); 1094 | 1095 | if (response.ok) { 1096 | console.log('✅ Webhook notification sent successfully'); 1097 | } else { 1098 | console.warn(`⚠️ Webhook notification failed: ${response.status} ${response.statusText}`); 1099 | } 1100 | } catch (webhookError) { 1101 | console.warn('⚠️ Webhook notification error:', webhookError.message); 1102 | } 1103 | } 1104 | 1105 | - name: Notify on Success 1106 | if: steps.claude.outcome == 'success' && needs.setup.outputs.issue-number 1107 | id: generate_success_comment 1108 | uses: actions/github-script@v7 1109 | with: 1110 | script: | 1111 | const isPR = '${{ needs.setup.outputs.is-pr }}' === 'true'; 1112 | const contextType = isPR ? 'Pull Request' : 'Issue'; 1113 | const eventName = '${{ github.event_name }}'; 1114 | const framework = '${{ steps.project-info.outputs.framework }}' || 'Unknown'; 1115 | const totalFiles = '${{ steps.project-info.outputs.total-files }}' || '0'; 1116 | const hasChanges = '${{ steps.check-changes.outputs.has-changes }}' === 'true'; 1117 | const autoPrNumber = '${{ steps.auto-pr.outputs.pr-number }}'; 1118 | const autoPrUrl = '${{ steps.auto-pr.outputs.pr-url }}'; 1119 | const branchName = '${{ steps.auto-pr.outputs.branch-name }}'; 1120 | const managementSuccess = '${{ steps.advanced-management.outputs.management-success }}' === 'true'; 1121 | const managementResults = '${{ steps.advanced-management.outputs.management-results }}'; 1122 | const owner = context.repo.owner 1123 | const repo = context.repo.repo 1124 | 1125 | const eventIcons = { 1126 | 'pull_request_target': '🔀', 1127 | 'issue_comment': '💬', 1128 | 'issues': '📋', 1129 | 'pull_request_review_comment': '📝', 1130 | 'pull_request_review': '👀' 1131 | }; 1132 | const icon = eventIcons[eventName] || '🤖'; 1133 | const hashSymbol = '#'; 1134 | 1135 | let message = `${icon} **Claude Code execution complete**\n\n`; 1136 | 1137 | // Result of automatic PR creation 1138 | if (!isPR && hasChanges && autoPrNumber) { 1139 | message += `🎉 **Automatic PR created successfully!**\n`; 1140 | message += `- PR: [${hashSymbol}${autoPrNumber}](${autoPrUrl})\n`; 1141 | message += `- Branch: \`${branchName}\`\n`; 1142 | message += `- Next step: Review PR → Merge\n\n`; 1143 | } else if (!isPR && !hasChanges) { 1144 | message += `ℹ️ **Analysis only** (no code changes)\n\n`; 1145 | } 1146 | 1147 | // Execution info (compact version) 1148 | message += `**📊 Execution Info:** ${contextType} ${hashSymbol}${${{ needs.setup.outputs.issue-number }}} | ${framework} (${totalFiles} files) | @${{ github.actor }}\n`; 1149 | if (isPR) { 1150 | message += `**🌿 Branch:** \`${{ needs.setup.outputs.head-ref }}\` → \`${{ needs.setup.outputs.base-ref }}\`\n`; 1151 | } 1152 | message += `**ℹ️ Action Run:** https://github.com/${owner}/${repo}/actions/runs/${{ github.run_id }}\n` 1153 | 1154 | // Repository management results (summary) 1155 | if (managementSuccess && managementResults) { 1156 | try { 1157 | const results = JSON.parse(managementResults); 1158 | const sections = ['labels', 'milestones', 'projects', 'security', 'wiki', 'pages', 'actions']; 1159 | const hasResults = sections.some(s => results[s] && results[s].length > 0); 1160 | 1161 | if (hasResults) { 1162 | // Check if there are actually any results to show 1163 | let hasDisplayableResults = false; 1164 | const displayableResults = []; 1165 | 1166 | sections.forEach(section => { 1167 | if (results[section] && results[section].length > 0) { 1168 | const summary = results[section].filter(r => r.includes('✅') || r.includes('⚠️')).slice(0, 2); 1169 | if (summary.length > 0) { 1170 | hasDisplayableResults = true; 1171 | displayableResults.push(...summary.map(r => `- ${r}`)); 1172 | } 1173 | } 1174 | }); 1175 | 1176 | if (hasDisplayableResults) { 1177 | message += `\n**🚀 Automated management executed:**\n`; 1178 | message += displayableResults.join('\n') + '\n'; 1179 | } 1180 | } 1181 | } catch (error) { 1182 | // Do not display in case of error 1183 | } 1184 | } 1185 | 1186 | message += `\n---\n`; 1187 | message += `💡 **Example commands for Claude:**\n\n`; 1188 | 1189 | // Main commands by category (concise version) 1190 | const commands = { 1191 | '🔍 Analysis & Review': [ 1192 | 'Please review the code', 1193 | 'Please perform a security check', 1194 | 'Please suggest performance improvements' 1195 | ], 1196 | '🛠️ Tasks & Implementation': [ 1197 | 'Please add test cases', 1198 | 'Please fix this issue and create a PR', 1199 | 'Please suggest refactoring' 1200 | ], 1201 | '📚 Management & Operations': [ 1202 | 'Please create a release', 1203 | 'Please check security alerts', 1204 | 'Please optimize the workflow' 1205 | ] 1206 | }; 1207 | 1208 | if (isPR) { 1209 | commands['🔀 PR Specific'] = [ 1210 | 'Please perform a final check before merging', 1211 | 'Please check for breaking changes' 1212 | ]; 1213 | } else { 1214 | commands['📋 Issue Specific'] = [ 1215 | 'Please investigate the root cause of this issue', 1216 | 'Please propose multiple solutions' 1217 | ]; 1218 | } 1219 | 1220 | Object.entries(commands).forEach(([category, cmds]) => { 1221 | message += `**${category}:**\n`; 1222 | cmds.forEach(cmd => message += `- \`claude ${cmd}\`\n`); 1223 | message += `\n`; 1224 | }) 1225 | 1226 | message += `🔄 **Rerun**: You can run again anytime with \`claude [your instructions]\``; 1227 | 1228 | // Add sticky comment identifier 1229 | message += `\n`; 1230 | 1231 | // Set as output steps.generate_success_comment.outputs.result 1232 | // return message; 1233 | core.setOutput('comment-body', message) 1234 | 1235 | - name: Notify on Failure 1236 | if: steps.claude.outcome == 'failure' && needs.setup.outputs.issue-number 1237 | id: generate_error_comment 1238 | uses: actions/github-script@v7 1239 | with: 1240 | script: | 1241 | const isPR = '${{ needs.setup.outputs.is-pr }}' === 'true'; 1242 | const contextType = isPR ? 'Pull Request' : 'Issue'; 1243 | const managementError = '${{ steps.advanced-management.outputs.management-error }}'; 1244 | const hashSymbol = '#'; 1245 | 1246 | let message = `❌ **Claude Code execution failed**\n\n`; 1247 | message += `An error occurred while processing ${contextType} ${hashSymbol}${{ needs.setup.outputs.issue-number }}.\n\n`; 1248 | 1249 | // Error info (compact version) 1250 | message += `**📊 Error Info:** ${contextType} | \`${{ github.event_name }}\` | @${{ github.actor }}\n`; 1251 | message += `**🔗 Detailed Log:** [Actions run log](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})\n\n`; 1252 | 1253 | if (managementError) { 1254 | message += `⚠️ **Repository Management Error:** \`${managementError}\`\n\n`; 1255 | } 1256 | 1257 | // Main causes and solutions (concise list) 1258 | message += `**🤔 Possible Causes and Solutions:**\n\n`; 1259 | 1260 | message += `**1. Temporary Issue** (most common)\n`; 1261 | message += `- Temporary limitations of the Claude API\n`; 1262 | message += `- → **Solution**: Rerun with \`claude\` after 5 minutes\n\n`; 1263 | 1264 | message += `**2. Timeout** (15-minute limit)\n`; 1265 | message += `- The task may be too complex\n`; 1266 | message += `- → **Solution**: Break down the task with more specific instructions\n\n`; 1267 | 1268 | message += `**3. Configuration Issue**\n`; 1269 | message += `- Expired or misconfigured tokens\n`; 1270 | message += `- → **Solution**: Check in Settings → Secrets → Actions\n`; 1271 | message += ` - \`CLAUDE_ACCESS_TOKEN\`\n`; 1272 | message += ` - \`CLAUDE_REFRESH_TOKEN\`\n`; 1273 | message += ` - \`CLAUDE_EXPIRES_AT\`\n\n`; 1274 | 1275 | // Concise rerun guide 1276 | message += `**💡 Rerun Tips:**\n`; 1277 | message += `- Be specific: \`claude review src/components/Button.tsx\`\n`; 1278 | message += `- Step by step: Start with one file\n`; 1279 | message += `- Be clear: State the expected outcome\n\n`; 1280 | 1281 | message += `---\n`; 1282 | message += `🔄 **Rerun**: Please try again with \`claude [specific instructions]\`\n`; 1283 | message += `📞 **Support**: If the problem persists, please contact an administrator`; 1284 | 1285 | message += `\n`; 1286 | 1287 | // Set as output steps.generate_error_comment.outputs.result 1288 | // return message; 1289 | core.setOutput('comment-body', message) 1290 | 1291 | # Update the sticky comment with the success or error message 1292 | - name: Post or Update Sticky Comment 1293 | if: | 1294 | always() 1295 | && (steps.generate_success_comment.outputs.comment-body || steps.generate_error_comment.outputs.comment-body) 1296 | && needs.setup.outputs.status-comment-id 1297 | uses: peter-evans/create-or-update-comment@v4 1298 | with: 1299 | issue-number: ${{ needs.setup.outputs.issue-number }} 1300 | # Pass the ID found in the previous step to ensure we UPDATE 1301 | comment-id: ${{ needs.setup.outputs.status-comment-id }} 1302 | # The body comes from your script 1303 | body: | 1304 | ${{ steps.generate_success_comment.outputs.comment-body || steps.generate_error_comment.outputs.comment-body }} 1305 | # Use 'replace' to overwrite the old "in-progress" message 1306 | edit-mode: replace 1307 | 1308 | - name: Output Execution Log 1309 | if: always() 1310 | run: | 1311 | echo "📊 ===== Execution Summary =====" 1312 | echo "Status: ${{ steps.claude.outcome }}" 1313 | echo "Context Type: ${{ needs.setup.outputs.is-pr == 'true' && 'PR' || 'Issue' }}" 1314 | echo "Issue/PR: '#${{ needs.setup.outputs.issue-number }}'" 1315 | echo "Branch: ${{ needs.setup.outputs.head-ref || 'default' }}" 1316 | echo "Actor: ${{ github.actor }}" 1317 | echo "Event: ${{ github.event_name }}" 1318 | echo "Project: ${{ steps.project-info.outputs.framework || 'Unknown' }}" 1319 | echo "Files: ${{ steps.project-info.outputs.total-files || '0' }}" 1320 | echo "Duration: ${{ steps.claude.outputs.duration || 'N/A' }}" 1321 | echo "" 1322 | echo "🔧 === Auto PR Creation Result ===" 1323 | echo "Has Changes: ${{ steps.check-changes.outputs.has-changes || 'N/A' }}" 1324 | echo "Auto PR Number: ${{ steps.auto-pr.outputs.pr-number || 'N/A' }}" 1325 | echo "Auto PR URL: ${{ steps.auto-pr.outputs.pr-url || 'N/A' }}" 1326 | echo "Branch Name: ${{ steps.auto-pr.outputs.branch-name || 'N/A' }}" 1327 | echo "Auto PR Error: ${{ steps.auto-pr.outputs.error || 'None' }}" 1328 | echo "" 1329 | echo "🚀 === Advanced Repository Management Result ===" 1330 | echo "Management Success: ${{ steps.advanced-management.outputs.management-success || 'N/A' }}" 1331 | echo "Management Error: ${{ steps.advanced-management.outputs.management-error || 'None' }}" 1332 | echo "Management Results Available: ${{ steps.advanced-management.outputs.management-results && 'Yes' || 'No' }}" 1333 | echo "" 1334 | echo "Timestamp: $(date -u)" 1335 | echo "==============================" --------------------------------------------------------------------------------