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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .devcontainer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.*Marker
54 changes: 54 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
FROM docker.io/rust:1-slim-trixie

# Install development dependencies
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
build-essential pkg-config musl-tools ca-certificates man-db \
neovim less git curl wget unzip ripgrep locales file procps net-tools \
nix-bin \
bubblewrap socat \
&& apt-get clean && rm -rf /var/lib/apt/lists/*

# Locale setup
RUN echo 'en_US.UTF-8 UTF-8' > /etc/locale.gen \
&& locale-gen en_US.UTF-8 \
&& update-locale LANG=en_US.UTF-8
ENV LANG=en_US.UTF-8
ENV LC_ALL=en_US.UTF-8

# Create non-root user
RUN groupadd -r dev -g 1000 \
&& useradd -r -g dev -u 1000 -d /home/dev -s /bin/bash dev \
&& mkdir /home/dev && chown dev:dev /home/dev

# Create directories with user ownership
# Note: VS Code Dev Containers will automatically adjust UID/GID via updateRemoteUserUID
RUN mkdir -p /code && chown dev:dev /code
RUN mkdir -p /nix && chown dev:dev /nix

# Switch to non-root user
USER dev

# Install Rust dev components
RUN rustup component add rustfmt clippy
RUN cargo install cargo-insta cargo-audit

# Create persistent config directory with default ownership
RUN mkdir -p ~/.config-persistent/

# Configure Nix
RUN mkdir -p ~/.config/nix \
&& printf 'store = local?root=/\nexperimental-features = nix-command flakes' > ~/.config/nix/nix.conf

# Install Nix packages
ENV PATH="${PATH}:/home/dev/.nix-profile/bin"
RUN nix profile install nixpkgs#alejandra

# Create Claude Code directories with default ownership
RUN mkdir -p ~/.config/claude/

# Create OpenCode directories with default ownership
RUN mkdir -p ~/.config/opencode/ \
&& mkdir -p ~/.local/share/opencode/

ENTRYPOINT ["/bin/bash"]
123 changes: 123 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
{
"name": "XC Bot Devcontainer",
"$schema": "https://raw.githubusercontent.com/devcontainers/spec/refs/heads/main/schemas/devContainer.schema.json",

"build": {
"dockerfile": "Dockerfile"
},

// Container runtime options
"runArgs": [
// Basic security: Limited capabilities
"--cap-drop=ALL",
"--cap-add=CHOWN",
"--cap-add=SETGID",
],

// Run as non-root user
"remoteUser": "dev",

// Automatically update container user UID/GID to match host user
"updateRemoteUserUID": true,

// Set working directory (devcontainer mounts workspace here)
"workspaceMount": "source=${localWorkspaceFolder},target=/code,type=bind,consistency=cached",
"workspaceFolder": "/code",

// Commands run after creating devcontainer
"initializeCommand": {
"create-claude-files": "mkdir -p ${HOME}/.config/claude/commands && for f in settings.json .credentials.json .claude.json; do [ -f ${HOME}/.config/claude/$f ] || echo '{}' > ${HOME}/.config/claude/$f; done && touch ${HOME}/.config/claude/CLAUDE.md",
"create-opencode-config-dir": "mkdir -p ${HOME}/.config/opencode",
"create-opencode-auth-file": "mkdir -p ${HOME}/.local/share/opencode && touch ${HOME}/.local/share/opencode/auth.json",
},

// Mounts
"mounts": [
// Persistent config (project-specific named volume for bash history etc.)
"source=devcontainer--${localWorkspaceFolderBasename}--config-persistent,target=/home/dev/.config-persistent,type=volume",
// Claude Code state (project-specific named volume for sessions, todos, statsig, etc.)
"source=devcontainer--${localWorkspaceFolderBasename}--claude-data,target=/home/dev/.config/claude,type=volume",
// Claude Code config and auth (shared across projects, bind-mounted on top of volume)
"source=${localEnv:HOME}/.config/claude/settings.json,target=/home/dev/.config/claude/settings.json,type=bind,consistency=cached",
"source=${localEnv:HOME}/.config/claude/CLAUDE.md,target=/home/dev/.config/claude/CLAUDE.md,type=bind,consistency=cached",
"source=${localEnv:HOME}/.config/claude/commands,target=/home/dev/.config/claude/commands,type=bind,consistency=cached",
"source=${localEnv:HOME}/.config/claude/.credentials.json,target=/home/dev/.config/claude/.credentials.json,type=bind,consistency=cached",
"source=${localEnv:HOME}/.config/claude/.claude.json,target=/home/dev/.config/claude/.claude.json,type=bind,consistency=cached",
// OpenCode config (shared across projects)
"source=${localEnv:HOME}/.config/opencode,target=/home/dev/.config/opencode,type=bind,consistency=cached",
// OpenCode data storage (project-specific named volume for sessions, messages, etc.)
"source=devcontainer--${localWorkspaceFolderBasename}--opencode-data,target=/home/dev/.local/share/opencode,type=volume",
// OpenCode credentials (shared from host, overlays on top of the volume)
"source=${localEnv:HOME}/.local/share/opencode/auth.json,target=/home/dev/.local/share/opencode/auth.json,type=bind,consistency=cached",
// Nix store
"source=devcontainer--${localWorkspaceFolderBasename}--nix,target=/nix,type=volume",
],

// Container environment variables
"containerEnv": {
// Bash history
"HISTFILE": "/home/dev/.config-persistent/.bash_history",
// Claude Code
"CLAUDE_CONFIG_DIR": "/home/dev/.config/claude/",
},

// Features to install
"features": {
"./features/claude-code": {
"version": "latest"
},
"./features/opencode": {
"version": "latest",
"binaryType": "glibc"
},
},

// Forward ports
"forwardPorts": [],
"appPort": [], // Compat with CLI, see https://github.com/devcontainers/cli/issues/22

// After creating container
"postCreateCommand": "echo 'export PATH=\"$PATH:/home/dev/.local/bin\"' >> /home/dev/.bashrc",

// After first start
"postStartCommand": "",

// Customizations
"customizations": {
"vscode": {
"settings": {
// General
"editor.defaultFormatter": "esbenp.prettier-vscode",
"files.insertFinalNewline": true,
"editor.formatOnSave": true,
// Extensions
"rewrap.wrappingColumn": 110,
// JSON
"[jsonc]": {
"json.format.keepLines": true
},
// Rust
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer"
},
"rust-analyzer.check.command": "clippy"
// Nix
"[nix]": {
"editor.defaultFormatter": "kamadorueda.alejandra"
},
},
"extensions": [
// General
"dnut.rewrap-revived",
"esbenp.prettier-vscode",
// Rust
"rust-lang.rust-analyzer",
"tamasfe.even-better-toml",
// Nix
"kamadorueda.alejandra",
"bbenoist.nix",
]
}
},

}
160 changes: 160 additions & 0 deletions .devcontainer/features/claude-code/claudecode-install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
#!/bin/bash

# Source: https://claude.ai/install.sh

set -e

# Parse command line arguments
TARGET="$1" # Optional target parameter

# Validate target if provided
if [[ -n "$TARGET" ]] && [[ ! "$TARGET" =~ ^(stable|latest|[0-9]+\.[0-9]+\.[0-9]+(-[^[:space:]]+)?)$ ]]; then
echo "Usage: $0 [stable|latest|VERSION]" >&2
exit 1
fi

GCS_BUCKET="https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases"
DOWNLOAD_DIR="$HOME/.claude/downloads"

# Check for required dependencies
DOWNLOADER=""
if command -v curl >/dev/null 2>&1; then
DOWNLOADER="curl"
elif command -v wget >/dev/null 2>&1; then
DOWNLOADER="wget"
else
echo "Either curl or wget is required but neither is installed" >&2
exit 1
fi

# Check if jq is available (optional)
HAS_JQ=false
if command -v jq >/dev/null 2>&1; then
HAS_JQ=true
fi

# Download function that works with both curl and wget
download_file() {
local url="$1"
local output="$2"

if [ "$DOWNLOADER" = "curl" ]; then
if [ -n "$output" ]; then
curl -fsSL -o "$output" "$url"
else
curl -fsSL "$url"
fi
elif [ "$DOWNLOADER" = "wget" ]; then
if [ -n "$output" ]; then
wget -q -O "$output" "$url"
else
wget -q -O - "$url"
fi
else
return 1
fi
}

# Simple JSON parser for extracting checksum when jq is not available
get_checksum_from_manifest() {
local json="$1"
local platform="$2"

# Normalize JSON to single line and extract checksum
json=$(echo "$json" | tr -d '\n\r\t' | sed 's/ \+/ /g')

# Extract checksum for platform using bash regex
if [[ $json =~ \"$platform\"[^}]*\"checksum\"[[:space:]]*:[[:space:]]*\"([a-f0-9]{64})\" ]]; then
echo "${BASH_REMATCH[1]}"
return 0
fi

return 1
}

# Detect platform
case "$(uname -s)" in
Darwin) os="darwin" ;;
Linux) os="linux" ;;
MINGW*|MSYS*|CYGWIN*) echo "Windows is not supported by this script. See https://code.claude.com/docs for installation options." >&2; exit 1 ;;
*) echo "Unsupported operating system: $(uname -s). See https://code.claude.com/docs for supported platforms." >&2; exit 1 ;;
esac

case "$(uname -m)" in
x86_64|amd64) arch="x64" ;;
arm64|aarch64) arch="arm64" ;;
*) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;;
esac

# Detect Rosetta 2 on macOS: if the shell is running as x64 under Rosetta on an ARM Mac,
# download the native arm64 binary instead of the x64 one
if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then
if [ "$(sysctl -n sysctl.proc_translated 2>/dev/null)" = "1" ]; then
arch="arm64"
fi
fi

# Check for musl on Linux and adjust platform accordingly
if [ "$os" = "linux" ]; then
if [ -f /lib/libc.musl-x86_64.so.1 ] || [ -f /lib/libc.musl-aarch64.so.1 ] || ldd /bin/ls 2>&1 | grep -q musl; then
platform="linux-${arch}-musl"
else
platform="linux-${arch}"
fi
else
platform="${os}-${arch}"
fi
mkdir -p "$DOWNLOAD_DIR"

# Always download latest version (which has the most up-to-date installer)
version=$(download_file "$GCS_BUCKET/latest")

# Download manifest and extract checksum
manifest_json=$(download_file "$GCS_BUCKET/$version/manifest.json")

# Use jq if available, otherwise fall back to pure bash parsing
if [ "$HAS_JQ" = true ]; then
checksum=$(echo "$manifest_json" | jq -r ".platforms[\"$platform\"].checksum // empty")
else
checksum=$(get_checksum_from_manifest "$manifest_json" "$platform")
fi

# Validate checksum format (SHA256 = 64 hex characters)
if [ -z "$checksum" ] || [[ ! "$checksum" =~ ^[a-f0-9]{64}$ ]]; then
echo "Platform $platform not found in manifest" >&2
exit 1
fi

# Download and verify
binary_path="$DOWNLOAD_DIR/claude-$version-$platform"
if ! download_file "$GCS_BUCKET/$version/$platform/claude" "$binary_path"; then
echo "Download failed" >&2
rm -f "$binary_path"
exit 1
fi

# Pick the right checksum tool
if [ "$os" = "darwin" ]; then
actual=$(shasum -a 256 "$binary_path" | cut -d' ' -f1)
else
actual=$(sha256sum "$binary_path" | cut -d' ' -f1)
fi

if [ "$actual" != "$checksum" ]; then
echo "Checksum verification failed" >&2
rm -f "$binary_path"
exit 1
fi

chmod +x "$binary_path"

# Run claude install to set up launcher and shell integration
echo "Setting up Claude Code..."
"$binary_path" install ${TARGET:+"$TARGET"}

# Clean up downloaded file
rm -f "$binary_path"

echo ""
echo "✅ Installation complete!"
echo ""
17 changes: 17 additions & 0 deletions .devcontainer/features/claude-code/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"id": "claude-code",
"version": "1.0.0",
"name": "Claude Code",
"description": "Installs Claude Code CLI agent tool",
"documentationURL": "https://code.claude.com/docs/en/setup",
"options": {
"version": {
"type": "string",
"default": "latest",
"description": "Version of Claude Code to install (e.g., '1.0.58', 'stable' or 'latest')"
}
},
"installsAfter": [
"ghcr.io/devcontainers/features/common-utils"
]
}
8 changes: 8 additions & 0 deletions .devcontainer/features/claude-code/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail

# Feature options
CLAUDE_CODE_VERSION="${VERSION:-latest}"

echo "Installing Claude Code as non-root user..."
su -s /bin/bash "$_REMOTE_USER" -c "bash $(pwd)/claudecode-install.sh $CLAUDE_CODE_VERSION"
Loading
Loading