Refactor devcontainer setup with MCP servers and dynamic config

Ajout d'une configuration DevContainer complète avec MCP Servers (LSP multi-langages, GitHub/Gitea), scripts de setup automatisés (crazycoder-setup.sh, setup-devcontainer.sh), et optimisation de l’image Docker (rustup/cargo-binstall, nettoyage des artefacts). Mise à jour du README avec documentation MCP et gestion dynamique des variables d’environnement (.env généré automatiquement selon la forge détectée). Simplification des entrypoints et postStartCommand pour une initialisation plus fiable.
This commit is contained in:
2026-03-11 22:18:02 +01:00
parent d6fb4e33e5
commit 6556c84e29
9 changed files with 405 additions and 184 deletions

View File

@@ -0,0 +1,9 @@
# Copier ce fichier en .env et renseigner les valeurs
# Le fichier .env est ignoré par git
# GitHub
GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx
# Gitea
GITEA_URL=https://gitea.example.com
GITEA_TOKEN=xxxxxxxxxxxxxxxxxxxx

View File

@@ -1,7 +1,7 @@
{ {
"name": "PMOCrazyCoder", "name": "PMOCrazyCoder",
"dockerComposeFile": "docker-compose.yml", "dockerComposeFile": "docker-compose.yml",
"service": "pmocrazy-coder", "service": "pmocrazycoder",
"workspaceFolder": "/workspace", "workspaceFolder": "/workspace",
"remoteUser": "devuser", "remoteUser": "devuser",
"settings": { "settings": {
@@ -9,5 +9,6 @@
}, },
"features": {}, "features": {},
"postCreateCommand": "npm install -g @anthropic-ai/claude-code", "postCreateCommand": "npm install -g @anthropic-ai/claude-code",
"postStartCommand": "/usr/local/bin/crazycoder-setup.sh",
"shutdownAction": "stopCompose", "shutdownAction": "stopCompose",
} }

View File

@@ -1,5 +1,5 @@
services: services:
pmocrazy-coder: pmocrazycoder:
image: niepce.petite-maison-orange.fr/public/pmocrazycoder:latest image: niepce.petite-maison-orange.fr/public/pmocrazycoder:latest
volumes: volumes:
- ..:/workspace - ..:/workspace
@@ -13,12 +13,16 @@ services:
- ./config/go-cache:/home/devuser/.cache/go - ./config/go-cache:/home/devuser/.cache/go
- ./config/go-bin:/home/devuser/go/bin - ./config/go-bin:/home/devuser/go/bin
- ./config/python-local:/home/devuser/.local - ./config/python-local:/home/devuser/.local
- ./config/zed:/home/devuser/.local/share/zed
- ./config/npm-cache:/home/devuser/.npm - ./config/npm-cache:/home/devuser/.npm
- ./config/r-libs:/home/devuser/R - ./config/r-libs:/home/devuser/R
working_dir: /workspace working_dir: /workspace
environment: environment:
- LOCAL_LLM_API=http://host.docker.internal:1248/v1 - LOCAL_LLM_API=http://host.docker.internal:1248/v1
- CLAUDE_CONFIG_DIR=/home/devuser/.claude - CLAUDE_CONFIG_DIR=/home/devuser/.claude
- GITHUB_TOKEN=${GITHUB_TOKEN:-}
- GITEA_URL=${GITEA_URL:-}
- GITEA_TOKEN=${GITEA_TOKEN:-}
user: devuser user: devuser
entrypoint: ["/usr/local/bin/entrypoint.sh"] entrypoint: ["/usr/local/bin/entrypoint.sh"]
command: ["bash", "-c", "tail -f /dev/null"] command: ["bash", "-c", "tail -f /dev/null"]

View File

@@ -1,60 +1,75 @@
FROM debian:bookworm-slim FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y \ # Base system packages — no golang-go/rustc/cargo from apt (replaced by rustup + install_latest_go.sh)
clang clangd llvm-dev libclang-dev pkg-config build-essential git make curl nodejs npm \ RUN apt-get update && apt-get install -y --no-install-recommends \
pylint black flake8 mypy isort logrotate sudo cmake libicu-dev zlib1g-dev \ clang clangd pkg-config build-essential git make curl \
libcurl4-openssl-dev libssl-dev ruby-dev jq python3 python3-pip golang-go rustc cargo \ nodejs npm \
r-base sqlite3 wget bash zsh libgit2-dev \ python3 python3-pip pylint black flake8 mypy isort \
logrotate sudo cmake zlib1g-dev \
libcurl4-openssl-dev libssl-dev \
ruby-dev libicu-dev \
jq \
r-base \
sqlite3 wget bash zsh \
&& apt-get clean && rm -rf /var/lib/apt/lists/* && apt-get clean && rm -rf /var/lib/apt/lists/*
RUN echo "deb http://deb.debian.org/debian testing main" > /etc/apt/sources.list.d/testing.list # Go — latest via script only (no apt golang-go)
COPY tools/install_latest_go.sh /tmp/install_latest_go.sh COPY tools/install_latest_go.sh /tmp/install_latest_go.sh
RUN bash /tmp/install_latest_go.sh && \ RUN bash /tmp/install_latest_go.sh && rm /tmp/install_latest_go.sh
echo "PATH=/usr/local/go/bin:$PATH" >> /root/.bashrc
ENV PATH=/usr/local/go/bin:$PATH ENV PATH=/usr/local/go/bin:$PATH
# Rust — rustup only (no apt rustc/cargo), stable only, no rust-docs
ENV RUSTUP_HOME=/usr/local/rustup ENV RUSTUP_HOME=/usr/local/rustup
ENV CARGO_HOME=/usr/local/cargo ENV CARGO_HOME=/usr/local/cargo
ENV PATH=/usr/local/cargo/bin:$PATH ENV PATH=/usr/local/cargo/bin:$PATH
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable && \ RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable --no-modify-path \
rustup install nightly && \ && rustup component add rustfmt clippy rust-src rust-analyzer \
rustup component add rustfmt clippy rust-src rust-docs && \ && rustup install nightly --no-self-update \
cargo install cargo-edit cargo-watch cargo-outdated cargo-make cargo-binstall && rustup component add rust-analyzer --toolchain nightly \
# Install cargo tools via binstall (prebuilt binaries, much faster & smaller)
&& cargo install cargo-binstall \
&& cargo binstall --no-confirm \
cargo-watch cargo-outdated cargo-make \
jj-cli aichat \
&& cargo install cargo-edit \
&& cargo install --git https://github.com/nilskch/jj-lsp.git \
# Clean build cache (keep installed binaries, drop registry & build artifacts)
&& rm -rf $CARGO_HOME/registry $CARGO_HOME/git /tmp/cargo-*
RUN npm install -g typescript tsc @types/node eslint prettier pyright typescript-language-server # Node tools
RUN npm install -g typescript @types/node eslint prettier pyright typescript-language-server \
mcp-server-commands \
&& npm cache clean --force
RUN cargo binstall --strategies crate-meta-data jj-cli aichat \ # Ruby tools
&& cargo install --git https://github.com/nilskch/jj-lsp.git RUN gem install github-linguist \
&& gem cleanup \
RUN gem install github-linguist && rm -rf /var/cache/gem /root/.gem/specs
# Python tools
RUN pip install --no-cache-dir --break-system-packages ruff fastmcp ipython tasklin RUN pip install --no-cache-dir --break-system-packages ruff fastmcp ipython tasklin
RUN curl -fsSL https://opencode.ai/install | bash && \ # Go tools
mkdir -p /root/.config/opencode && \
mv /root/.opencode/bin/opencode /usr/local/bin/
ENV GOPATH=/usr/local/go ENV GOPATH=/usr/local/go
ENV GOBIN=/usr/local/go/bin ENV GOBIN=/usr/local/go/bin
RUN go install golang.org/x/tools/gopls@latest \ RUN go install golang.org/x/tools/gopls@latest \
&& go install github.com/isaacphi/mcp-language-server@latest && go install github.com/isaacphi/mcp-language-server@latest \
&& go install github.com/github/github-mcp-server/cmd/github-mcp-server@latest \
&& go install gitea.com/gitea/gitea-mcp@latest \
&& go clean -cache -modcache
# Shell completions
RUN mkdir -p /etc/bash_completion.d \ RUN mkdir -p /etc/bash_completion.d \
&& curl https://raw.githubusercontent.com/sigoden/aichat/main/scripts/completions/aichat.bash \ && curl -fsSL https://raw.githubusercontent.com/sigoden/aichat/main/scripts/completions/aichat.bash \
> /etc/bash_completion.d/aichat \ > /etc/bash_completion.d/aichat \
&& jj util completion bash > /etc/bash_completion.d/jj && jj util completion bash > /etc/bash_completion.d/jj
RUN rustup component add rust-analyzer --toolchain nightly # Non-root user
ARG USERNAME=devuser ARG USERNAME=devuser
ARG USER_UID=1000 ARG USER_UID=1000
ARG USER_GID=1000 ARG USER_GID=1000
RUN groupadd --gid $USER_GID $USERNAME && \ RUN groupadd --gid $USER_GID $USERNAME && \
useradd --uid $USER_UID \ useradd --uid $USER_UID --gid $USER_GID -m $USERNAME --shell /bin/bash && \
--gid $USER_GID \
-m $USERNAME \
--shell /bin/bash && \
echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME && \ echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME && \
chmod 440 /etc/sudoers.d/$USERNAME chmod 440 /etc/sudoers.d/$USERNAME
@@ -64,7 +79,9 @@ RUN chmod +x /usr/local/bin/entrypoint.sh
COPY bin/* /usr/local/bin/ COPY bin/* /usr/local/bin/
RUN chmod +x /usr/local/bin/* RUN chmod +x /usr/local/bin/*
RUN mkdir -p /home/${USERNAME}/.config \ RUN mkdir -p /home/${USERNAME}/.config \
&& chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.config && mkdir -p /home/${USERNAME}/.cache \
&& chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.config \
&& chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.cache
COPY --chown=${USERNAME}:${USERNAME} opencode /home/${USERNAME}/.config/opencode COPY --chown=${USERNAME}:${USERNAME} opencode /home/${USERNAME}/.config/opencode
USER $USERNAME USER $USERNAME

View File

@@ -94,13 +94,14 @@ build-multiplatform: .builder-ready
|| docker buildx create --name $(BUILDER) --driver docker-container --bootstrap || docker buildx create --name $(BUILDER) --driver docker-container --bootstrap
touch .builder-ready touch .builder-ready
## push : Push the OCI archive to the registry using skopeo ## push : Push the OCI archive to the registry using skopeo, then pull locally
push: $(OCI_ARCHIVE) push: $(OCI_ARCHIVE)
skopeo copy \ skopeo copy \
--dest-tls-verify=true \ --dest-tls-verify=true \
--multi-arch all \ --multi-arch all \
oci-archive:$(OCI_ARCHIVE) \ oci-archive:$(OCI_ARCHIVE) \
docker://$(FULL_IMAGE) docker://$(FULL_IMAGE)
docker pull $(FULL_IMAGE)
# Ensure the OCI archive exists before pushing # Ensure the OCI archive exists before pushing
$(OCI_ARCHIVE): $(OCI_ARCHIVE):

View File

@@ -25,11 +25,58 @@ Environnement de développement conteneurisé avec assistance IA intégrée —
- `sqlite3`, `cmake`, `make`, `git` - `sqlite3`, `cmake`, `make`, `git`
- `aichat` (client LLM CLI) - `aichat` (client LLM CLI)
### MCP Servers installés
Le conteneur inclut plusieurs MCP servers pour l'intégration avec les outils de développement :
| MCP Server | Utilisation |
|---|---|
| `mcp-language-server` | LSP pour Go (gopls), Python (pyright-langserver), Rust (rust-analyzer), TypeScript |
| `github-mcp-server` | Intégration GitHub (repos, issues, PR) via `GITHUB_TOKEN` |
| `gitea-mcp` | Intégration Gitea/Forgejo via `GITEA_URL` et `GITEA_TOKEN` |
| `mcp-server-commands` | Exécution de commandes shell depuis l'assistant IA |
### Assistants IA ### Assistants IA
- **Claude Code** (installé au `postCreateCommand`) - **Claude Code** (installé au `postCreateCommand`)
- **opencode** (configuré via l'entrypoint) - **opencode** (configuré via l'entrypoint)
- **aichat** (configuré via l'entrypoint, connecté au LLM local) - **aichat** (configuré via l'entrypoint, connecté au LLM local)
### Configuration MCP dans les assistants
Les MCP servers sont automatiquement configurés dans opencode et Claude Code via `crazycoder-setup.sh` (exécuté en `postStartCommand`) :
**opencode** (`~/.config/opencode/opencode.json`) :
- LSP pour Go, Python, Rust, TypeScript via `mcp-language-server`
- Commandes shell via `mcp-server-commands`
- GitHub MCP (si `GITHUB_TOKEN` défini)
- Gitea MCP (si `GITEA_URL` et `GITEA_TOKEN` définis)
**Claude Code** (`~/.claude/.claude.json`) :
- Même configuration MCP qu'opencode
Les configurations sont générées dynamiquement au démarrage du conteneur avec les variables d'environnement disponibles.
## Variables d'environnement (.env)
Le fichier `.env` (non versionné, ignoré par git) contient les credentials pour les services externes :
| Variable | Description | Exemple |
|---|---|---|
| `GITHUB_TOKEN` | Personal Access Token GitHub pour l'accès API | `ghp_xxxxxxxxxxxxxxxxxxxx` |
| `GITEA_URL` | URL de votre serveur Gitea/Forgejo | `https://gitea.example.com` |
| `GITEA_TOKEN` | Token d'accès API Gitea | `xxxxxxxxxxxxxxxxxxxx` |
Copiez `.devcontainer/.env.example` vers `.env` et renseignez vos tokens :
```bash
cp .devcontainer/.env.example .env
# Éditer .env avec vos credentials
```
Ces variables sont utilisées par :
- Le MCP Server GitHub (pour les opérations repo/issues/PR)
- Le MCP Server Gitea (intégration avec votre instance)
## Utilisation rapide ## Utilisation rapide
### Ajouter l'environnement à un projet existant ### Ajouter l'environnement à un projet existant
@@ -89,6 +136,22 @@ pip install --user monpaquet
R -e 'install.packages("tidyverse")' R -e 'install.packages("tidyverse")'
``` ```
## Configuration MCP détaillée
### opencode
Le fichier `~/.config/opencode/opencode.json` est généré à chaque démarrage avec :
- **LSP servers** : gopls, pyright-langserver, rust-analyzer, typescript-language-server
- **Shell commands** : `mcp-server-commands`
- **GitHub MCP** : activé si `GITHUB_TOKEN` est défini
- **Gitea MCP** : activé si `GITEA_URL` et `GITEA_TOKEN` sont définis
- **Provider LM Studio** : endpoint local configurable
### Claude Code
Le fichier `~/.claude/.claude.json` est mis à jour avec la section `mcpServers` contenant les mêmes serveurs que opencode.
## LLM local ## LLM local
L'entrypoint configure automatiquement opencode et aichat pour pointer sur `LOCAL_LLM_API` L'entrypoint configure automatiquement opencode et aichat pour pointer sur `LOCAL_LLM_API`

191
bin/crazycoder-setup.sh Normal file
View File

@@ -0,0 +1,191 @@
#!/bin/bash
# crazycoder-setup.sh
# Runs via postStartCommand — configures aichat, opencode and Claude Code MCP.
# Executed by Zed/VS Code/Cursor after the container starts, as remoteUser (devuser).
export USERNAME=${USERNAME:-devuser}
export WORKSPACE=${WORKSPACE:-/workspace}
export LOCAL_LLM_API=${LOCAL_LLM_API:-http://host.docker.internal:1248/v1}
export LLM_MODEL=${LLM_MODEL:-'ollama:qwen/qwen3-coder-next@4bit'}
export AICHAT_API_BASE=$(sed -E 's@(https?://[^/]+).*$@\1@' <<< $LOCAL_LLM_API)
export MAXTOKEN=${MAXTOKEN:-4096}
export NUM_CTX=${NUM_CTX:-32768}
export REPEAT_PENALTY=${REPEAT_PENALTY:-1.3}
export TEMPERATURE=${TEMPERATURE:-0.5}
export TOP_P=${TOP_P:-0.95}
export TOP_K=${TOP_K:-40}
export STOP=${STOP:-"[\"</s>\", \"\\n\\n\", \"def \", \"class \"]"}
# --- Clear Zed external_agents so Zed can re-download without "Directory not empty" ---
rm -rf "$HOME/.local/share/zed/external_agents"
# --- Initialise .ai_index si nécessaire ---
if [ ! -d "$WORKSPACE/.ai_index" ]; then
mkdir -p "$WORKSPACE/.ai_index/ast"
mkdir -p "$WORKSPACE/.ai_index/embeddings"
touch "$WORKSPACE/.ai_index/graph.db"
fi
# --- Configure PATH dans .bashrc (une seule fois) ---
if ! grep -q "LOCAL_LLM_API" "$HOME/.bashrc"; then
echo "export LOCAL_LLM_API=$LOCAL_LLM_API" >> "$HOME/.bashrc"
echo "export AICHAT_API_BASE=$AICHAT_API_BASE" >> "$HOME/.bashrc"
echo "export PATH=\$HOME/.cargo/bin:\$HOME/go/bin:\$HOME/.local/bin:\$PATH" >> "$HOME/.bashrc"
fi
# --- Configure aichat ---
mkdir -p "$HOME/.config/aichat"
cat > "$HOME/.config/aichat/config.yaml" <<EOF
model: lmstudio:${LLM_MODEL#*:}
clients:
- type: openai-compatible
name: lmstudio
api_base: ${LOCAL_LLM_API}
api_key: placeholder
models:
- name: ${LLM_MODEL#*:}
max_input_tokens: ${NUM_CTX}
temperature: ${TEMPERATURE}
top_p: ${TOP_P}
EOF
# --- Configure opencode ---
RUST_ANALYZER=$(rustup which --toolchain nightly rust-analyzer 2>/dev/null || true)
mkdir -p "$HOME/.config/opencode"
jq -n \
--arg workspace "$WORKSPACE" \
--arg gopath "$GOPATH" \
--arg rust_analyzer "$RUST_ANALYZER" \
--arg llm_api "$LOCAL_LLM_API" \
--arg llm_model "${LLM_MODEL#*:}" \
--arg full_model "$LLM_MODEL" \
--arg github_token "${GITHUB_TOKEN:-}" \
--arg gitea_url "${GITEA_URL:-}" \
--arg gitea_token "${GITEA_TOKEN:-}" \
--argjson temperature "$TEMPERATURE" \
--argjson top_p "$TOP_P" \
--argjson top_k "$TOP_K" \
--argjson max_tokens "$MAXTOKEN" \
--argjson num_ctx "$NUM_CTX" \
--argjson repeat_penalty "$REPEAT_PENALTY" \
--argjson stop "$STOP" \
'{
"$schema": "https://opencode.ai/config.json",
"mcp": ({
"go-lsp-tools": {
"type": "local",
"command": ["mcp-language-server", "--workspace", $workspace, "--lsp", "gopls"],
"enabled": true,
"environment": { "GOPATH": $gopath }
},
"python-lsp-tools": {
"type": "local",
"command": ["mcp-language-server", "--workspace", $workspace, "--lsp", "pyright-langserver", "--", "--stdio"],
"enabled": true
},
"rust-lsp-tools": {
"type": "local",
"command": ["mcp-language-server", "--workspace", $workspace, "--lsp", $rust_analyzer],
"enabled": true
},
"typescript-lsp-tools": {
"type": "local",
"command": ["mcp-language-server", "--workspace", $workspace, "--lsp", "typescript-language-server", "--", "--stdio"],
"enabled": true
},
"shell": {
"type": "local",
"command": ["mcp-server-commands"],
"enabled": true
},
"github": (if $github_token != "" then {
"type": "local",
"command": ["github-mcp-server", "stdio"],
"enabled": true,
"environment": { "GITHUB_PERSONAL_ACCESS_TOKEN": $github_token }
} else null end),
"gitea": (if ($gitea_token != "" and $gitea_url != "") then {
"type": "local",
"command": ["gitea-mcp", "-t", "stdio", "--host", $gitea_url, "--token", $gitea_token],
"enabled": true
} else null end)
} | with_entries(select(.value != null))),
"provider": {
"lmstudio": {
"npm": "@ai-sdk/openai-compatible",
"name": "LM Studio (local)",
"options": { "baseURL": $llm_api, "apiKey": "" },
"models": {
"qwen/qwen3-coder-next@4bit": {
"name": "Qwen 3 Coder Next 80B — (LLMStudio ; 4-bit)",
"limit": { "context": 200000, "output": 65536 },
"settings": { "temperature": $temperature, "top_p": $top_p, "top_k": $top_k, "max_tokens": $max_tokens, "num_ctx": $num_ctx, "repeat_penalty": $repeat_penalty, "stop": $stop }
},
"qwen/qwen3-coder-next@6bit": {
"name": "Qwen 3 Coder Next 80B — (LLMStudio ; 6-bit)",
"limit": { "context": 200000, "output": 65536 },
"settings": { "temperature": $temperature, "top_p": $top_p, "top_k": $top_k, "max_tokens": $max_tokens, "num_ctx": $num_ctx, "repeat_penalty": $repeat_penalty, "stop": $stop }
}
}
}
},
"model": $full_model
}' > "$HOME/.config/opencode/opencode.json"
# --- Configure MCP pour Claude Code ---
MCP_SERVERS=$(jq -n \
--arg workspace "$WORKSPACE" \
--arg gopath "$GOPATH" \
--arg rust_analyzer "$RUST_ANALYZER" \
--arg github_token "${GITHUB_TOKEN:-}" \
--arg gitea_url "${GITEA_URL:-}" \
--arg gitea_token "${GITEA_TOKEN:-}" \
'{
"go-lsp-tools": {
"type": "stdio",
"command": "mcp-language-server",
"args": ["--workspace", $workspace, "--lsp", "gopls"],
"env": { "GOPATH": $gopath }
},
"python-lsp-tools": {
"type": "stdio",
"command": "mcp-language-server",
"args": ["--workspace", $workspace, "--lsp", "pyright-langserver", "--", "--stdio"]
},
"rust-lsp-tools": {
"type": "stdio",
"command": "mcp-language-server",
"args": ["--workspace", $workspace, "--lsp", $rust_analyzer]
},
"typescript-lsp-tools": {
"type": "stdio",
"command": "mcp-language-server",
"args": ["--workspace", $workspace, "--lsp", "typescript-language-server", "--", "--stdio"]
},
"github": (if $github_token != "" then {
"type": "stdio",
"command": "github-mcp-server",
"args": ["stdio"],
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": $github_token }
} else null end),
"shell": {
"type": "stdio",
"command": "mcp-server-commands"
},
"gitea": (if ($gitea_token != "" and $gitea_url != "") then {
"type": "stdio",
"command": "gitea-mcp",
"args": ["-t", "stdio", "--host", $gitea_url, "--token", $gitea_token]
} else null end)
} | with_entries(select(.value != null))')
CLAUDE_JSON="$HOME/.claude/.claude.json"
mkdir -p "$HOME/.claude"
if [ -f "$CLAUDE_JSON" ]; then
jq --argjson mcp "$MCP_SERVERS" '. + {mcpServers: $mcp}' "$CLAUDE_JSON" > "${CLAUDE_JSON}.tmp" \
&& mv "${CLAUDE_JSON}.tmp" "$CLAUDE_JSON"
else
jq -n --argjson mcp "$MCP_SERVERS" '{mcpServers: $mcp}' > "$CLAUDE_JSON"
fi

View File

@@ -1,150 +1,15 @@
#!/bin/bash #!/bin/bash
# docker-entrypoint.sh # docker-entrypoint.sh — minimal, just sets up PATH and execs the command
export USERNAME=${USERNAME:-devuser} export PATH=/usr/local/cargo/bin:/usr/local/go/bin:$HOME/.local/bin:$PATH
export WORKSPACE=${WORKSPACE:-/workspace} # Set npm prefix to user directory so global installs don't require root
export LOCAL_LLM_API=${LOCAL_LLM_API:-http://host.docker.internal:1248/v1} npm config set prefix "$HOME/.local"
export LLM_MODEL=${LLM_MODEL:-'ollama:qwen/qwen3-coder-next@4bit'}
export AICHAT_API_BASE=$(sed -E 's@(https?://[^/]+).*$@\1@' <<< $LOCAL_LLM_API)
export MAXTOKEN=${MAXTOKEN:-4096}
export NUM_CTX=${NUM_CTX:-32768}
export REPEAT_PENALTY=${REPEAT_PENALTY:-1.3}
export TEMPERATURE=${TEMPERATURE:-0.5}
export TOP_P=${TOP_P:-0.95}
export TOP_K=${TOP_K:-40}
export STOP=${STOP:-"[\"</s>\", \"\\n\\n\", \"def \", \"class \"]"}
# --- Initialise .ai_index si nécessaire --- # Clear Zed external_agents on each start so Zed can re-download without
if [ ! -d "/workspace/.ai_index" ]; then # hitting "Directory not empty" on atomic rename.
mkdir -p /workspace/.ai_index/ast rm -rf "$HOME/.local/share/zed/external_agents"
mkdir -p /workspace/.ai_index/embeddings
touch /workspace/.ai_index/graph.db
fi
# --- Configure LOCAL_LLM_API pour Bash ---
if [ -n "$LOCAL_LLM_API" ]; then
if ! grep -q "LOCAL_LLM_API" /home/${USERNAME}/.bashrc; then
echo "export LOCAL_LLM_API=$LOCAL_LLM_API" >> /home/${USERNAME}/.bashrc
echo "export AICHAT_API_BASE=$AICHAT_API_BASE" >> /home/${USERNAME}/.bashrc
echo "export PATH=\$HOME/.cargo/bin:\$HOME/go/bin:\$HOME/.local/bin:\$PATH" >> /home/${USERNAME}/.bashrc
fi
# --- Configure aichat ---
mkdir -p /home/${USERNAME}/.config/aichat
AICHAT_CONFIG=/home/${USERNAME}/.config/aichat/config.yaml
cat > $AICHAT_CONFIG <<EOF
model: lmstudio:${LLM_MODEL#*:}
clients:
- type: openai-compatible
name: lmstudio
api_base: ${LOCAL_LLM_API}
api_key: placeholder
models:
- name: ${LLM_MODEL#*:}
max_input_tokens: ${NUM_CTX}
temperature: ${TEMPERATURE}
top_p: ${TOP_P}
EOF
chown ${USERNAME}:${USERNAME} ${AICHAT_CONFIG}
mkdir -p /home/${USERNAME}/.config/opencode
CONFIG_FILE=/home/${USERNAME}/.config/opencode/opencode.json
cat > $CONFIG_FILE <<EOF
{
"\$schema": "https://opencode.ai/config.json",
"mcp": {
"go-lsp-tools": {
"type": "local",
"command": ["mcp-language-server",
"--workspace", "$WORKSPACE",
"--lsp", "gopls"
],
"enabled": true,
"environment": {
"GOPATH": "$GOPATH"
}
},
"python-lsp-tools": {
"type": "local",
"command": ["mcp-language-server",
"--workspace", "$WORKSPACE",
"--lsp", "pyright-langserver",
"--", "--stdio"
],
"enabled": true
},
"rust-lsp-tools": {
"type": "local",
"command": ["mcp-language-server",
"--workspace", "$WORKSPACE",
"--lsp", "$(find /usr/local/rustup/toolchains -type f -name rust-analyzer)"
],
"enabled": true
},
"typescript-lsp-tools": {
"type": "local",
"command": ["mcp-language-server",
"--workspace", "$WORKSPACE",
"--lsp", "typescript-language-server",
"--", "--stdio"
],
"enabled": true
},
},
"provider": {
"lmstudio": {
"npm": "@ai-sdk/openai-compatible",
"name": "LM Studio (local)",
"options": {
"baseURL": "$LOCAL_LLM_API",
"apiKey": ""
},
"models": {
"qwen/qwen3-coder-next@4bit": {
"name": "Qwen 3 Coder Next 80B — (LLMStudio ; 4-bit)",
"limit": {
"context": 200000,
"output": 65536
},
"settings": {
"temperature": $TEMPERATURE,
"top_p": $TOP_P,
"top_k": $TOP_K,
"max_tokens": ${MAXTOKEN},
"num_ctx": ${NUM_CTX},
"repeat_penalty": ${REPEAT_PENALTY},
"stop": ${STOP}
}
},
"qwen/qwen3-coder-next@6bit": {
"name": "Qwen 3 Coder Next 80B — (LLMStudio ; 6-bit)",
"limit": {
"context": 200000,
"output": 65536
},
"settings": {
"temperature": $TEMPERATURE,
"top_p": $TOP_P,
"top_k": $TOP_K,
"max_tokens": ${MAXTOKEN},
"num_ctx": ${NUM_CTX},
"repeat_penalty": ${REPEAT_PENALTY},
"stop": ${STOP}
}
}
}
}
},
"model": "${LLM_MODEL}"
}
EOF
chown ${USERNAME}:${USERNAME} ${CONFIG_FILE}
fi
# --- Force Bash comme shell par défaut ---
if [ -z "$1" ]; then if [ -z "$1" ]; then
exec /bin/bash exec /bin/bash
else else

View File

@@ -46,6 +46,7 @@ mkdir -p "$DEVCONTAINER_DIR/config/cargo-bin"
mkdir -p "$DEVCONTAINER_DIR/config/go-cache" mkdir -p "$DEVCONTAINER_DIR/config/go-cache"
mkdir -p "$DEVCONTAINER_DIR/config/go-bin" mkdir -p "$DEVCONTAINER_DIR/config/go-bin"
mkdir -p "$DEVCONTAINER_DIR/config/python-local" mkdir -p "$DEVCONTAINER_DIR/config/python-local"
mkdir -p "$DEVCONTAINER_DIR/config/zed"
mkdir -p "$DEVCONTAINER_DIR/config/npm-cache" mkdir -p "$DEVCONTAINER_DIR/config/npm-cache"
mkdir -p "$DEVCONTAINER_DIR/config/r-libs" mkdir -p "$DEVCONTAINER_DIR/config/r-libs"
@@ -55,8 +56,13 @@ if [[ -f "$DEVCONTAINER_JSON" ]]; then
warning "devcontainer.json already exists, skipping" warning "devcontainer.json already exists, skipping"
else else
fetch ".devcontainer/devcontainer.json" "$DEVCONTAINER_JSON" fetch ".devcontainer/devcontainer.json" "$DEVCONTAINER_JSON"
# Rename the service name to match the project PROJECT_NAME="$(basename "$PROJECT_DIR")"
sed -i.bak "s/\"name\": \"PMOCrazyCoder\"/\"name\": \"$(basename "$PROJECT_DIR")\"/" "$DEVCONTAINER_JSON" SERVICE_NAME="crazycoder-${PROJECT_NAME}"
# Patch display name and service reference
sed -i.bak \
-e "s/\"name\": \"PMOCrazyCoder\"/\"name\": \"${PROJECT_NAME}\"/" \
-e "s/\"service\": \"pmocrazycoder\"/\"service\": \"${SERVICE_NAME}\"/" \
"$DEVCONTAINER_JSON"
rm -f "$DEVCONTAINER_JSON.bak" rm -f "$DEVCONTAINER_JSON.bak"
info "Created devcontainer.json" info "Created devcontainer.json"
fi fi
@@ -67,33 +73,97 @@ if [[ -f "$COMPOSE_FILE" ]]; then
warning "docker-compose.yml already exists, skipping" warning "docker-compose.yml already exists, skipping"
else else
fetch ".devcontainer/docker-compose.yml" "$COMPOSE_FILE" fetch ".devcontainer/docker-compose.yml" "$COMPOSE_FILE"
PROJECT_NAME="$(basename "$PROJECT_DIR")"
SERVICE_NAME="crazycoder-${PROJECT_NAME}"
# Rename the service to avoid collision with project's own Docker services
sed -i.bak "s/^ pmocrazycoder:/ ${SERVICE_NAME}:/" "$COMPOSE_FILE"
# Patch LOCAL_LLM_API if overridden # Patch LOCAL_LLM_API if overridden
if [[ "$LOCAL_LLM_API" != "http://host.docker.internal:1248/v1" ]]; then if [[ "$LOCAL_LLM_API" != "http://host.docker.internal:1248/v1" ]]; then
sed -i.bak "s|http://host.docker.internal:1248/v1|${LOCAL_LLM_API}|g" "$COMPOSE_FILE" sed -i.bak "s|http://host.docker.internal:1248/v1|${LOCAL_LLM_API}|g" "$COMPOSE_FILE"
rm -f "$COMPOSE_FILE.bak"
fi fi
rm -f "$COMPOSE_FILE.bak"
info "Created docker-compose.yml" info "Created docker-compose.yml"
fi fi
# --- .env ---
ENV_FILE="$DEVCONTAINER_DIR/.env"
if [[ -f "$ENV_FILE" ]]; then
warning ".env already exists, skipping"
else
# Détecter la forge depuis l'origin git du projet
DETECTED_GITEA_URL=""
IS_GITHUB=false
if command -v git >/dev/null && git -C "$PROJECT_DIR" rev-parse --git-dir &>/dev/null; then
ORIGIN_URL=$(git -C "$PROJECT_DIR" remote get-url origin 2>/dev/null || true)
if [[ -n "$ORIGIN_URL" ]]; then
# Extraire scheme + host (supporte https://host/... et git@host:...)
if [[ "$ORIGIN_URL" =~ ^(https?://[^/]+) ]]; then
ORIGIN_HOST="${BASH_REMATCH[1]}"
elif [[ "$ORIGIN_URL" =~ ^git@([^:]+): ]]; then
ORIGIN_HOST="https://${BASH_REMATCH[1]}"
fi
if [[ "${ORIGIN_HOST:-}" == *"github.com"* ]]; then
IS_GITHUB=true
info "Forge détectée : GitHub"
elif [[ -n "${ORIGIN_HOST:-}" ]]; then
# Tester si c'est un Gitea/Forgejo via son API
if curl -fsSL --max-time 5 "${ORIGIN_HOST}/api/v1/version" 2>/dev/null | grep -q '"version"'; then
DETECTED_GITEA_URL="$ORIGIN_HOST"
info "Forge détectée : Gitea/Forgejo ($DETECTED_GITEA_URL)"
else
warning "Forge non reconnue pour $ORIGIN_HOST (ni GitHub ni Gitea/Forgejo)"
fi
fi
fi
fi
{
echo "# Secrets du devcontainer — ne pas committer"
echo ""
if $IS_GITHUB; then
echo "# GitHub"
echo "GITHUB_TOKEN="
else
echo "# GitHub (non détecté pour ce projet)"
echo "# GITHUB_TOKEN="
fi
echo ""
if [[ -n "$DETECTED_GITEA_URL" ]]; then
echo "# Gitea"
echo "GITEA_URL=${DETECTED_GITEA_URL}"
echo "GITEA_TOKEN="
else
echo "# Gitea (non détecté pour ce projet)"
echo "# GITEA_URL="
echo "# GITEA_TOKEN="
fi
} > "$ENV_FILE"
info "Created .env"
fi
# --- .gitignore --- # --- .gitignore ---
GITIGNORE="$PROJECT_DIR/.gitignore" GITIGNORE="$PROJECT_DIR/.gitignore"
if [[ -f "$GITIGNORE" ]]; then if [[ -f "$GITIGNORE" ]]; then
if grep -q "\.devcontainer/config/" "$GITIGNORE"; then if grep -q "\.devcontainer/config/" "$GITIGNORE"; then
warning ".gitignore already contains .devcontainer/config/, skipping" warning ".gitignore already contains .devcontainer/config/, skipping"
else else
printf '\n# PMOCrazyCoder devcontainer runtime config\n.devcontainer/config/\n' >> "$GITIGNORE" printf '\n# PMOCrazyCoder devcontainer runtime config\n.devcontainer/config/\n.devcontainer/.env\n' >> "$GITIGNORE"
info "Updated .gitignore" info "Updated .gitignore"
fi fi
else else
printf '# PMOCrazyCoder devcontainer runtime config\n.devcontainer/config/\n' > "$GITIGNORE" printf '# PMOCrazyCoder devcontainer runtime config\n.devcontainer/config/\n.devcontainer/.env\n' > "$GITIGNORE"
info "Created .gitignore" info "Created .gitignore"
fi fi
echo echo
info "Done. Next steps:" info "Done. Next steps:"
echo " 1. Open the project in VS Code / Zed / Cursor" echo " 1. Renseigner les tokens dans .devcontainer/.env"
echo " 2. Reopen in container (the image will be pulled from the registry)" echo " 2. Open the project in VS Code / Zed / Cursor"
echo " 3. Run /login in the Claude Agent panel to authenticate" echo " 3. Reopen in container (the image will be pulled from the registry)"
echo " 4. Run /login in the Claude Agent panel to authenticate"
echo echo
info "To use a different local LLM API endpoint:" info "To use a different local LLM API endpoint:"
echo " LOCAL_LLM_API=http://myserver:1248/v1 bash setup-devcontainer.sh" echo " LOCAL_LLM_API=http://myserver:1248/v1 bash setup-devcontainer.sh"