Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .devcontainer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.zsh_history
.generated/
68 changes: 26 additions & 42 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
}
}
18 changes: 18 additions & 0 deletions .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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
78 changes: 78 additions & 0 deletions .devcontainer/scripts/bootstrap-tools.sh
Original file line number Diff line number Diff line change
@@ -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
105 changes: 105 additions & 0 deletions .devcontainer/scripts/generate-local-override.sh
Original file line number Diff line number Diff line change
@@ -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"
72 changes: 72 additions & 0 deletions .devcontainer/scripts/probe-host-tools.sh
Original file line number Diff line number Diff line change
@@ -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'
16 changes: 16 additions & 0 deletions .devcontainer/scripts/setup-shell-history.sh
Original file line number Diff line number Diff line change
@@ -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"
15 changes: 15 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 && \
Expand Down Expand Up @@ -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
Expand Down