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:
9
.devcontainer/.env.example
Normal file
9
.devcontainer/.env.example
Normal 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
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "PMOCrazyCoder",
|
||||
"dockerComposeFile": "docker-compose.yml",
|
||||
"service": "pmocrazy-coder",
|
||||
"service": "pmocrazycoder",
|
||||
"workspaceFolder": "/workspace",
|
||||
"remoteUser": "devuser",
|
||||
"settings": {
|
||||
@@ -9,5 +9,6 @@
|
||||
},
|
||||
"features": {},
|
||||
"postCreateCommand": "npm install -g @anthropic-ai/claude-code",
|
||||
"postStartCommand": "/usr/local/bin/crazycoder-setup.sh",
|
||||
"shutdownAction": "stopCompose",
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
services:
|
||||
pmocrazy-coder:
|
||||
pmocrazycoder:
|
||||
image: niepce.petite-maison-orange.fr/public/pmocrazycoder:latest
|
||||
volumes:
|
||||
- ..:/workspace
|
||||
@@ -13,12 +13,16 @@ services:
|
||||
- ./config/go-cache:/home/devuser/.cache/go
|
||||
- ./config/go-bin:/home/devuser/go/bin
|
||||
- ./config/python-local:/home/devuser/.local
|
||||
- ./config/zed:/home/devuser/.local/share/zed
|
||||
- ./config/npm-cache:/home/devuser/.npm
|
||||
- ./config/r-libs:/home/devuser/R
|
||||
working_dir: /workspace
|
||||
environment:
|
||||
- LOCAL_LLM_API=http://host.docker.internal:1248/v1
|
||||
- CLAUDE_CONFIG_DIR=/home/devuser/.claude
|
||||
- GITHUB_TOKEN=${GITHUB_TOKEN:-}
|
||||
- GITEA_URL=${GITEA_URL:-}
|
||||
- GITEA_TOKEN=${GITEA_TOKEN:-}
|
||||
user: devuser
|
||||
entrypoint: ["/usr/local/bin/entrypoint.sh"]
|
||||
command: ["bash", "-c", "tail -f /dev/null"]
|
||||
|
||||
79
Dockerfile
79
Dockerfile
@@ -1,60 +1,75 @@
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
clang clangd llvm-dev libclang-dev pkg-config build-essential git make curl nodejs npm \
|
||||
pylint black flake8 mypy isort logrotate sudo cmake libicu-dev zlib1g-dev \
|
||||
libcurl4-openssl-dev libssl-dev ruby-dev jq python3 python3-pip golang-go rustc cargo \
|
||||
r-base sqlite3 wget bash zsh libgit2-dev \
|
||||
# Base system packages — no golang-go/rustc/cargo from apt (replaced by rustup + install_latest_go.sh)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
clang clangd pkg-config build-essential git make curl \
|
||||
nodejs npm \
|
||||
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/*
|
||||
|
||||
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
|
||||
RUN bash /tmp/install_latest_go.sh && \
|
||||
echo "PATH=/usr/local/go/bin:$PATH" >> /root/.bashrc
|
||||
RUN bash /tmp/install_latest_go.sh && rm /tmp/install_latest_go.sh
|
||||
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 CARGO_HOME=/usr/local/cargo
|
||||
ENV PATH=/usr/local/cargo/bin:$PATH
|
||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable && \
|
||||
rustup install nightly && \
|
||||
rustup component add rustfmt clippy rust-src rust-docs && \
|
||||
cargo install cargo-edit cargo-watch cargo-outdated cargo-make cargo-binstall
|
||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable --no-modify-path \
|
||||
&& rustup component add rustfmt clippy rust-src rust-analyzer \
|
||||
&& rustup install nightly --no-self-update \
|
||||
&& 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 \
|
||||
&& cargo install --git https://github.com/nilskch/jj-lsp.git
|
||||
|
||||
RUN gem install github-linguist
|
||||
# Ruby tools
|
||||
RUN gem install github-linguist \
|
||||
&& gem cleanup \
|
||||
&& rm -rf /var/cache/gem /root/.gem/specs
|
||||
|
||||
# Python tools
|
||||
RUN pip install --no-cache-dir --break-system-packages ruff fastmcp ipython tasklin
|
||||
|
||||
RUN curl -fsSL https://opencode.ai/install | bash && \
|
||||
mkdir -p /root/.config/opencode && \
|
||||
mv /root/.opencode/bin/opencode /usr/local/bin/
|
||||
|
||||
# Go tools
|
||||
ENV GOPATH=/usr/local/go
|
||||
ENV GOBIN=/usr/local/go/bin
|
||||
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 \
|
||||
&& 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 \
|
||||
&& jj util completion bash > /etc/bash_completion.d/jj
|
||||
|
||||
RUN rustup component add rust-analyzer --toolchain nightly
|
||||
|
||||
# Non-root user
|
||||
ARG USERNAME=devuser
|
||||
ARG USER_UID=1000
|
||||
ARG USER_GID=1000
|
||||
RUN groupadd --gid $USER_GID $USERNAME && \
|
||||
useradd --uid $USER_UID \
|
||||
--gid $USER_GID \
|
||||
-m $USERNAME \
|
||||
--shell /bin/bash && \
|
||||
useradd --uid $USER_UID --gid $USER_GID -m $USERNAME --shell /bin/bash && \
|
||||
echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" > /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/
|
||||
RUN chmod +x /usr/local/bin/*
|
||||
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
|
||||
|
||||
USER $USERNAME
|
||||
|
||||
3
Makefile
3
Makefile
@@ -94,13 +94,14 @@ build-multiplatform: .builder-ready
|
||||
|| docker buildx create --name $(BUILDER) --driver docker-container --bootstrap
|
||||
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)
|
||||
skopeo copy \
|
||||
--dest-tls-verify=true \
|
||||
--multi-arch all \
|
||||
oci-archive:$(OCI_ARCHIVE) \
|
||||
docker://$(FULL_IMAGE)
|
||||
docker pull $(FULL_IMAGE)
|
||||
|
||||
# Ensure the OCI archive exists before pushing
|
||||
$(OCI_ARCHIVE):
|
||||
|
||||
63
README.md
63
README.md
@@ -25,11 +25,58 @@ Environnement de développement conteneurisé avec assistance IA intégrée —
|
||||
- `sqlite3`, `cmake`, `make`, `git`
|
||||
- `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
|
||||
- **Claude Code** (installé au `postCreateCommand`)
|
||||
- **opencode** (configuré via l'entrypoint)
|
||||
- **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
|
||||
|
||||
### Ajouter l'environnement à un projet existant
|
||||
@@ -89,6 +136,22 @@ pip install --user monpaquet
|
||||
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
|
||||
|
||||
L'entrypoint configure automatiquement opencode et aichat pour pointer sur `LOCAL_LLM_API`
|
||||
|
||||
191
bin/crazycoder-setup.sh
Normal file
191
bin/crazycoder-setup.sh
Normal 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
|
||||
@@ -1,150 +1,15 @@
|
||||
#!/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}
|
||||
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 \"]"}
|
||||
# Set npm prefix to user directory so global installs don't require root
|
||||
npm config set prefix "$HOME/.local"
|
||||
|
||||
# --- 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
|
||||
# Clear Zed external_agents on each start so Zed can re-download without
|
||||
# hitting "Directory not empty" on atomic rename.
|
||||
rm -rf "$HOME/.local/share/zed/external_agents"
|
||||
|
||||
# --- 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
|
||||
exec /bin/bash
|
||||
else
|
||||
|
||||
@@ -46,6 +46,7 @@ mkdir -p "$DEVCONTAINER_DIR/config/cargo-bin"
|
||||
mkdir -p "$DEVCONTAINER_DIR/config/go-cache"
|
||||
mkdir -p "$DEVCONTAINER_DIR/config/go-bin"
|
||||
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/r-libs"
|
||||
|
||||
@@ -55,8 +56,13 @@ if [[ -f "$DEVCONTAINER_JSON" ]]; then
|
||||
warning "devcontainer.json already exists, skipping"
|
||||
else
|
||||
fetch ".devcontainer/devcontainer.json" "$DEVCONTAINER_JSON"
|
||||
# Rename the service name to match the project
|
||||
sed -i.bak "s/\"name\": \"PMOCrazyCoder\"/\"name\": \"$(basename "$PROJECT_DIR")\"/" "$DEVCONTAINER_JSON"
|
||||
PROJECT_NAME="$(basename "$PROJECT_DIR")"
|
||||
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"
|
||||
info "Created devcontainer.json"
|
||||
fi
|
||||
@@ -67,33 +73,97 @@ if [[ -f "$COMPOSE_FILE" ]]; then
|
||||
warning "docker-compose.yml already exists, skipping"
|
||||
else
|
||||
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
|
||||
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"
|
||||
rm -f "$COMPOSE_FILE.bak"
|
||||
fi
|
||||
rm -f "$COMPOSE_FILE.bak"
|
||||
info "Created docker-compose.yml"
|
||||
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="$PROJECT_DIR/.gitignore"
|
||||
if [[ -f "$GITIGNORE" ]]; then
|
||||
if grep -q "\.devcontainer/config/" "$GITIGNORE"; then
|
||||
warning ".gitignore already contains .devcontainer/config/, skipping"
|
||||
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"
|
||||
fi
|
||||
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"
|
||||
fi
|
||||
|
||||
echo
|
||||
info "Done. Next steps:"
|
||||
echo " 1. Open the project in VS Code / Zed / Cursor"
|
||||
echo " 2. Reopen in container (the image will be pulled from the registry)"
|
||||
echo " 3. Run /login in the Claude Agent panel to authenticate"
|
||||
echo " 1. Renseigner les tokens dans .devcontainer/.env"
|
||||
echo " 2. Open the project in VS Code / Zed / Cursor"
|
||||
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
|
||||
info "To use a different local LLM API endpoint:"
|
||||
echo " LOCAL_LLM_API=http://myserver:1248/v1 bash setup-devcontainer.sh"
|
||||
|
||||
Reference in New Issue
Block a user