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

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