diff --git a/.devcontainer/.gitignore b/.devcontainer/.gitignore new file mode 100644 index 00000000..4ea2e1d8 --- /dev/null +++ b/.devcontainer/.gitignore @@ -0,0 +1,2 @@ +.zsh_history +.generated/ diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 872881d1..d885c395 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,44 +1,28 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile +// For format details, see https://containers.dev/implementors/json_reference/ { - "name": "rmcs-develop", - "image": "qzhhhi/rmcs-develop:latest-full", - "privileged": true, - "mounts": [ - { - "source": "/dev", - "target": "/dev", - "type": "bind" - }, - { - "source": "/tmp/.X11-unix", - "target": "/tmp/.X11-unix", - "type": "bind" - } - ], - "containerEnv": { - "DISPLAY": "${localEnv:DISPLAY}" - }, - "runArgs": [ - "--network", - "host" - ], - "customizations": { - "vscode": { - "extensions": [ - // C++ language support - "llvm-vs-code-extensions.vscode-clangd", - // Python language support - "ms-python.vscode-pylance", - "ms-python.python", - "ms-python.debugpy", - // CMake language support - "twxs.cmake", - // Code spell checking - "streetsidesoftware.code-spell-checker", - // Git enhancements - "mhutchie.git-graph" - ] - } - } + "name": "rmcs-develop", + "dockerComposeFile": [ + "docker-compose.yml", + ".generated/docker-compose.local.override.yml" + ], + "service": "rmcs-develop", + "workspaceFolder": "/workspaces/RMCS", + "initializeCommand": "bash .devcontainer/scripts/generate-local-override.sh && bash .devcontainer/scripts/probe-host-tools.sh", + "postCreateCommand": "bash .devcontainer/scripts/setup-shell-history.sh && bash .devcontainer/scripts/bootstrap-tools.sh", + "customizations": { + "vscode": { + "settings": { + "remote.autoForwardPorts": false + }, + "extensions": [ + "llvm-vs-code-extensions.vscode-clangd", + "ms-python.vscode-pylance", + "ms-python.python", + "ms-python.debugpy", + "KylinIdeTeam.cmake-intellisence", + "streetsidesoftware.code-spell-checker", + "mhutchie.git-graph" + ] + } + } } diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 00000000..2101fff6 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,18 @@ +services: + rmcs-develop: + image: qzhhhi/rmcs-develop:latest-full + privileged: true + network_mode: host + init: true + command: /bin/sh -c "while sleep 1000; do :; done" + working_dir: /home/ubuntu + volumes: + - type: bind + source: /dev + target: /dev + - type: bind + source: /tmp/.X11-unix + target: /tmp/.X11-unix + - type: bind + source: .. + target: /workspaces/RMCS diff --git a/.devcontainer/scripts/bootstrap-tools.sh b/.devcontainer/scripts/bootstrap-tools.sh new file mode 100644 index 00000000..c475d889 --- /dev/null +++ b/.devcontainer/scripts/bootstrap-tools.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +set -eu + +installed_tools="" +skipped_tools="" +manifest_path="/workspaces/RMCS/.devcontainer/.generated/host-tools.manifest" + +if [ ! -f "$manifest_path" ]; then + printf 'Warning: host tools manifest not found at %s\n' "$manifest_path" >&2 +fi + +if ! command -v npm >/dev/null 2>&1; then + printf 'Warning: npm is not available in the container, skipping host tool bootstrap.\n' >&2 + exit 0 +fi + +add_item() { + local current_list="$1" + local item="$2" + + if [ -z "$current_list" ]; then + printf '%s' "$item" + else + printf '%s\n%s' "$current_list" "$item" + fi +} + +install_tool() { + local tool_name="$1" + local package_name="$2" + + if [ ! -f "$manifest_path" ]; then + skipped_tools=$(add_item "$skipped_tools" "$tool_name (host manifest missing)") + return 0 + fi + + local manifest_line="" + local version="" + + manifest_line=$(grep -m 1 -E "^${tool_name}=" "$manifest_path" 2>/dev/null || true) + if [ -z "$manifest_line" ]; then + skipped_tools=$(add_item "$skipped_tools" "$tool_name (not installed on host)") + else + version=${manifest_line#*=} + if printf '%s' "$version" | grep -Eq '^(v)?[0-9]+([.][0-9]+)*([-.][0-9A-Za-z]+)*$'; then + version=${version#v} + if sudo npm install -g "$package_name@$version"; then + installed_tools=$(add_item "$installed_tools" "$tool_name@$version") + else + skipped_tools=$(add_item "$skipped_tools" "$tool_name (install failed for $version)") + fi + else + printf 'Warning: skipping %s because version could not be parsed on host\n' "$tool_name" >&2 + skipped_tools=$(add_item "$skipped_tools" "$tool_name (version could not be parsed on host)") + fi + fi +} + +install_tool "codex" "@openai/codex" +install_tool "claude" "@anthropic-ai/claude-code" +install_tool "opencode" "opencode-ai" +install_tool "lark-cli" "@larksuite/cli" + +printf 'Bootstrap summary:\n' +if [ -n "$installed_tools" ]; then + printf 'Installed:\n%s\n' "$installed_tools" +else + printf 'Installed: none\n' +fi + +if [ -n "$skipped_tools" ]; then + printf 'Skipped:\n%s\n' "$skipped_tools" +else + printf 'Skipped: none\n' +fi + +exit 0 diff --git a/.devcontainer/scripts/generate-local-override.sh b/.devcontainer/scripts/generate-local-override.sh new file mode 100644 index 00000000..44707194 --- /dev/null +++ b/.devcontainer/scripts/generate-local-override.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +set -eu + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +generated_dir="$SCRIPT_DIR/../.generated" +output_file="$generated_dir/docker-compose.local.override.yml" +repo_root="$(cd "$SCRIPT_DIR/../.." && pwd)" + +environment_entries="" +mounts="" + +escape_yaml_double_quoted() { + value=$1 + value=${value//\\/\\\\} + value=${value//\"/\\\"} + value=${value//$'\n'/\\n} + printf '%s' "$value" +} + +add_env() { + env_name=$1 + env_value=$2 + escaped_env_value=$(escape_yaml_double_quoted "$env_value") + + if [ -n "$environment_entries" ]; then + environment_entries="${environment_entries} ${env_name}: \"${escaped_env_value}\"\n" + else + environment_entries=" ${env_name}: \"${escaped_env_value}\"\n" + fi +} + +add_mount() { + host_path=$1 + target_path=$2 + + if [ -e "$host_path" ]; then + escaped_host_path=$(escape_yaml_double_quoted "$host_path") + escaped_target_path=$(escape_yaml_double_quoted "$target_path") + mounts="${mounts} - type: bind\n source: \"${escaped_host_path}\"\n target: \"${escaped_target_path}\"\n" + fi +} + +add_ro_mount() { + host_path=$1 + target_path=$2 + + if [ -e "$host_path" ]; then + escaped_host_path=$(escape_yaml_double_quoted "$host_path") + escaped_target_path=$(escape_yaml_double_quoted "$target_path") + mounts="${mounts} - type: bind\n source: \"${escaped_host_path}\"\n target: \"${escaped_target_path}\"\n read_only: true\n" + fi +} + +add_env "HOST_WORKSPACE_FOLDER" "$repo_root" + +if [ -n "${DISPLAY:-}" ]; then + add_env "DISPLAY" "$DISPLAY" +fi +if [ -n "${HTTP_PROXY:-}" ]; then + add_env "HTTP_PROXY" "$HTTP_PROXY" +fi +if [ -n "${HTTPS_PROXY:-}" ]; then + add_env "HTTPS_PROXY" "$HTTPS_PROXY" +fi +if [ -n "${NO_PROXY:-}" ]; then + add_env "NO_PROXY" "$NO_PROXY" +fi +if [ -n "${http_proxy:-}" ]; then + add_env "http_proxy" "$http_proxy" +fi +if [ -n "${https_proxy:-}" ]; then + add_env "https_proxy" "$https_proxy" +fi +if [ -n "${no_proxy:-}" ]; then + add_env "no_proxy" "$no_proxy" +fi + +add_mount "$HOME/.codex" "/home/ubuntu/.codex" +add_mount "$HOME/.claude" "/home/ubuntu/.claude" +add_mount "$HOME/.claude.json" "/home/ubuntu/.claude.json" +add_mount "$HOME/.lark-cli" "/home/ubuntu/.lark-cli" +add_mount "$HOME/.config/opencode" "/home/ubuntu/.config/opencode" +add_mount "$HOME/.local/share/lark-cli" "/home/ubuntu/.local/share/lark-cli" +add_mount "$HOME/.local/share/opencode" "/home/ubuntu/.local/share/opencode" +add_ro_mount "$HOME/.agents/skills" "/home/ubuntu/.agents/skills" + +mkdir -p "$generated_dir" + +{ + printf '%s\n' '# Generated by generate-local-override.sh - do not commit' + printf '%s\n' 'services:' + printf '%s\n' ' rmcs-develop:' + printf '%s\n' ' environment:' + if [ -n "$environment_entries" ]; then + printf '%b' "$environment_entries" + else + printf '%s\n' ' {}' + fi + printf '%s\n' ' volumes:' + if [ -n "$mounts" ]; then + printf '%b' "$mounts" + else + printf '%s\n' ' []' + fi +} > "$output_file" diff --git a/.devcontainer/scripts/probe-host-tools.sh b/.devcontainer/scripts/probe-host-tools.sh new file mode 100644 index 00000000..4be155e7 --- /dev/null +++ b/.devcontainer/scripts/probe-host-tools.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash + +set -eu + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +parse_version() { + local version_output="$1" + local parsed_version="" + local version_regex='v?[0-9]+([.][0-9]+)*([-.][0-9A-Za-z]+)*' + + if [[ $version_output =~ $version_regex ]]; then + parsed_version=${BASH_REMATCH[0]} + parsed_version=${parsed_version#v} + printf '%s' "$parsed_version" + return 0 + fi + + return 1 +} + +probe_tool() { + tool_name="$1" + + if ! command -v "$tool_name" >/dev/null 2>&1; then + printf '%s\n' 'absent' + return 0 + fi + + version_output="" + if version_output=$($tool_name --version 2>/dev/null); then + if version=$(parse_version "$version_output"); then + printf '%s\n' "$version" + else + printf '%s\n' 'unparseable' + fi + else + printf '%s\n' 'unparseable' + fi +} + +generated_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" +manifest_dir="$REPO_ROOT/.devcontainer/.generated" +manifest_path="$manifest_dir/host-tools.manifest" +tmp_manifest_path="$manifest_path.tmp" + +mkdir -p "$manifest_dir" + +codex_version="$(probe_tool codex)" +claude_version="$(probe_tool claude)" +opencode_version="$(probe_tool opencode)" +lark_cli_version="$(probe_tool lark-cli)" + +{ + printf '%s\n' '# Generated by probe-host-tools.sh - do not commit' + printf 'generated_at=%s\n' "$generated_at" + printf 'codex=%s\n' "$codex_version" + printf 'claude=%s\n' "$claude_version" + printf 'opencode=%s\n' "$opencode_version" + printf 'lark-cli=%s\n' "$lark_cli_version" +} > "$tmp_manifest_path" + +mv "$tmp_manifest_path" "$manifest_path" + +printf 'Host tool probe summary:\n' +printf ' codex=%s\n' "$codex_version" +printf ' claude=%s\n' "$claude_version" +printf ' opencode=%s\n' "$opencode_version" +printf ' lark-cli=%s\n' "$lark_cli_version" +printf ' manifest=%s\n' "$manifest_path" +printf 'Tip: rerun `bash /workspaces/RMCS/.devcontainer/scripts/bootstrap-tools.sh` to resync container tool versions with the host.\n' diff --git a/.devcontainer/scripts/setup-shell-history.sh b/.devcontainer/scripts/setup-shell-history.sh new file mode 100644 index 00000000..5b968270 --- /dev/null +++ b/.devcontainer/scripts/setup-shell-history.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -eu + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +history_path="$REPO_ROOT/.devcontainer/.zsh_history" +history_link="$HOME/.zsh_history" + +mkdir -p "$(dirname "$history_path")" +touch "$history_path" +rm -f "$history_link" +ln -s "$history_path" "$history_link" + +printf 'Shell history symlinked: %s -> %s\n' "$history_link" "$history_path" diff --git a/Dockerfile b/Dockerfile index c2844060..ba3854ac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -113,6 +113,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ apt-get autoremove -y && apt-get clean && \ rm -rf /var/lib/apt/lists/* /tmp/* +# Install Node.js 24 LTS (required by agent CLIs) +RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash - && \ + apt-get install -y --no-install-recommends nodejs && \ + apt-get autoremove -y && apt-get clean && \ + rm -rf /var/lib/apt/lists/* /tmp/* + # Install llvm-toolchain ARG LLVM_VERSION=22 RUN mkdir -p /etc/apt/keyrings && \ @@ -172,6 +178,15 @@ RUN case "${TARGETARCH}" in \ # Change user RUN chsh -s /bin/zsh ubuntu && \ echo "ubuntu ALL=(ALL:ALL) NOPASSWD:ALL" >> /etc/sudoers + +# Precreate generic XDG-style parent directories for direct bind mounts under ubuntu's home. +RUN mkdir -p \ + /home/ubuntu/.agents \ + /home/ubuntu/.cache \ + /home/ubuntu/.config \ + /home/ubuntu/.local/share \ + /home/ubuntu/.local/state && \ + chown -R ubuntu:ubuntu /home/ubuntu/.agents /home/ubuntu/.cache /home/ubuntu/.config /home/ubuntu/.local WORKDIR /home/ubuntu ENV USER=ubuntu ENV WORKDIR=/home/ubuntu