push-oynptmmzrplt #2
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