opencode
opencode copied to clipboard
[FEATURE]: Config debugging tool
Feature hasn't been suggested before.
- [x] I have verified this feature I'm about to request hasn't been suggested before.
Describe the enhancement you want to request
In my experience, json/jsonc config files can become quite brittle. It's good of course that you can combine multiple configs.
Still sometimes I wished an isolated environment like the following script - start Docker container, load opencode binary, be able to pass config files, dump logs. In case this appears useful to others, maybe ship it with future releases and mention in the docs.
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'USAGE'
Usage:
test-opencode.sh <main_config_path> [custom_config_path]
Runs OpenCode TUI in a fresh Debian container with isolated config.
- Installs opencode inside the container via curl install script.
- Global config -> ~/.config/opencode/opencode.json
- Optional custom config -> OPENCODE_CONFIG=/home/opencode/custom-config.json
- Shadows /work/opencode.json and /work/.opencode.json (prevents repo config merging)
- Uses a temp writable /work/.opencode (prevents repo pollution)
- On container shutdown, dumps opencode logs/state verbosely into local ./logs/ (NOT root-owned), no gzip
Env overrides:
OPENCODE_DOCKER_IMAGE default: debian:bookworm-slim
OPENCODE_WORKDIR host dir mounted at /work (default: current dir)
USAGE
}
die(){ echo "Error: $*" >&2; exit 1; }
need(){ command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"; }
need docker
need mktemp
[[ $# -ge 1 && $# -le 2 ]] || { usage; exit 2; }
MAIN_CFG="$1"
CUSTOM_CFG="${2:-}"
[[ -f "$MAIN_CFG" ]] || die "main_config_path not found: $MAIN_CFG"
if [[ -n "$CUSTOM_CFG" ]]; then
[[ -f "$CUSTOM_CFG" ]] || die "custom_config_path not found: $CUSTOM_CFG"
fi
abs_path() {
local p="$1"
if command -v realpath >/dev/null 2>&1; then
realpath "$p"
else
python3 - <<PY
import os,sys
print(os.path.abspath(sys.argv[1]))
PY
fi
}
MAIN_CFG="$(abs_path "$MAIN_CFG")"
if [[ -n "$CUSTOM_CFG" ]]; then CUSTOM_CFG="$(abs_path "$CUSTOM_CFG")"; fi
IMAGE="${OPENCODE_DOCKER_IMAGE:-debian:bookworm-slim}"
WORKDIR_HOST="$(abs_path "${OPENCODE_WORKDIR:-$PWD}")"
# Local logs dir (host): ./logs
LOGDIR_HOST="$WORKDIR_HOST/logs"
mkdir -p "$LOGDIR_HOST"
STAGE="$(mktemp -d)"
cleanup(){ rm -rf "$STAGE"; }
trap cleanup EXIT
# Stage isolated HOME with ONLY given configs
mkdir -p "$STAGE/home/opencode/.config/opencode"
cp -f "$MAIN_CFG" "$STAGE/home/opencode/.config/opencode/opencode.json"
if [[ -n "$CUSTOM_CFG" ]]; then
cp -f "$CUSTOM_CFG" "$STAGE/home/opencode/custom-config.json"
fi
# Shadow project-level config files so they won't merge from your repo
mkdir -p "$STAGE/shadow"
: > "$STAGE/shadow/opencode.json"
: > "$STAGE/shadow/.opencode.json"
# Writable isolated /work/.opencode (opencode writes package.json/deps here)
mkdir -p "$STAGE/work_opencode"
HOST_UID="$(id -u)"
HOST_GID="$(id -g)"
# Write container entrypoint script (avoids brittle nested quoting)
cat >"$STAGE/container-entry.sh" <<'EOS'
#!/usr/bin/env bash
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
echo "[container] Installing prerequisites..."
apt-get update -y >/dev/null
apt-get install -y --no-install-recommends \
ca-certificates curl tar findutils coreutils passwd >/dev/null
update-ca-certificates >/dev/null || true
echo "[container] Done."
# Create user/group matching host uid/gid so anything written to /work is not root-owned.
echo "[container] Ensuring user opencode matches host uid/gid..."
if ! getent group "${HOST_GID}" >/dev/null 2>&1; then
groupadd -g "${HOST_GID}" hostgrp
fi
if ! id -u "${HOST_UID}" >/dev/null 2>&1; then
useradd -m -u "${HOST_UID}" -g "${HOST_GID}" -d /home/opencode -s /bin/bash opencode
fi
echo "[container] User: $(id opencode || true)"
# Ensure local ./logs exists and is writable by the opencode user
mkdir -p /work/logs
chown "${HOST_UID}:${HOST_GID}" /work/logs 2>/dev/null || true
chmod u+rwx /work/logs 2>/dev/null || true
# Script that actually runs opencode as the non-root user
cat >/tmp/opencode-user.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
export HOME=/home/opencode
export XDG_CONFIG_HOME="$HOME/.config"
export XDG_CACHE_HOME="$HOME/.cache"
export XDG_STATE_HOME="$HOME/.local/state"
export PATH="$HOME/.opencode/bin:$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin"
mkdir -p "$XDG_CONFIG_HOME/opencode" "$XDG_CACHE_HOME" "$XDG_STATE_HOME"
echo "[opencode-user] Installing opencode (YOLO curl install)..."
curl -fsSL https://opencode.ai/install | bash
echo "[opencode-user] PATH=$PATH"
echo "[opencode-user] opencode resolved to: $(command -v opencode || true)"
echo
# Enable custom config if mounted
if [[ -f "$HOME/custom-config.json" ]]; then
export OPENCODE_CONFIG="$HOME/custom-config.json"
fi
echo "--- Configs visible in container ---"
echo "Global config dir: $XDG_CONFIG_HOME/opencode"
ls -la "$XDG_CONFIG_HOME/opencode" || true
if [[ -n "${OPENCODE_CONFIG-}" ]]; then
echo "Custom config: $OPENCODE_CONFIG"
ls -la "$OPENCODE_CONFIG" || true
else
echo "Custom config: none"
fi
echo "--- Project config shadow ---"
ls -la /work/opencode.json /work/.opencode.json || true
echo "--- Writable isolated project state ---"
ls -la /work/.opencode || true
echo "------------------------------------"
echo
exec opencode /work
EOF
chmod +x /tmp/opencode-user.sh
dump_logs() {
echo
echo "================= OpenCode log dump (verbose) ================="
echo "[dump] Triggered at: $(date -Is 2>/dev/null || date)"
echo "[dump] Container user (current): $(id || true)"
echo "[dump] Writing to: /work/logs (host: ./logs)"
echo "[dump] NO tar / NO gzip; copying files/directories directly."
echo "---------------------------------------------------------------"
# Ensure destination exists and is writable by the opencode user (host uid)
mkdir -p /work/logs
chown "${HOST_UID}:${HOST_GID}" /work/logs 2>/dev/null || true
chmod u+rwx /work/logs 2>/dev/null || true
local ts dest
ts="$(date +%Y%m%d-%H%M%S 2>/dev/null || true)"
ts="${ts:-unknown-time}"
dest="/work/logs/opencode-dump-${ts}"
echo "[dump] Output directory: ${dest}"
echo "[dump] Destination dir listing:"
ls -la /work/logs || true
echo
echo "[dump] Copying as user opencode (so output is NOT root-owned)..."
runuser -u opencode -- bash -lc "
set -euo pipefail
echo '[dump/opencode] Effective user:' \$(id)
mkdir -p \"$dest\"
export HOME=/home/opencode
export XDG_CONFIG_HOME=\"\$HOME/.config\"
export XDG_CACHE_HOME=\"\$HOME/.cache\"
export XDG_STATE_HOME=\"\$HOME/.local/state\"
echo '[dump/opencode] HOME='\"\$HOME\"
echo '[dump/opencode] XDG_CONFIG_HOME='\"\$XDG_CONFIG_HOME\"
echo '[dump/opencode] XDG_CACHE_HOME='\"\$XDG_CACHE_HOME\"
echo '[dump/opencode] XDG_STATE_HOME='\"\$XDG_STATE_HOME\"
echo
echo '[dump/opencode] Candidate paths (missing is OK):'
for p in \"\$HOME/.config/opencode\" \"\$HOME/.opencode\" \"\$HOME/.cache\" \"\$HOME/.local/state\" \"/work/.opencode\"; do
if [ -e \"\$p\" ]; then
echo \" - COPY: \$p\"
else
echo \" - SKIP (missing): \$p\"
fi
done
echo
echo '[dump/opencode] Copying into per-source subfolders to avoid collisions...'
for p in \"\$HOME/.config/opencode\" \"\$HOME/.opencode\" \"\$HOME/.cache\" \"\$HOME/.local/state\" \"/work/.opencode\"; do
if [ -e \"\$p\" ]; then
name=\$(echo \"\$p\" | sed 's#^/##' | tr \"/\" \"_\")
echo \" -> cp -a \$p $dest/\$name\"
cp -a \"\$p\" \"$dest/\$name\" 2>/dev/null || true
echo \" listing (top 20):\"
ls -la \"$dest/\$name\" 2>/dev/null | head -n 20 || true
echo
fi
done
echo '[dump/opencode] Finished. Dump directory contents:'
ls -la \"$dest\" || true
"
echo
echo "[dump] Post-write ownership check:"
ls -la "$dest" || true
echo "==============================================================="
echo
}
cleanup() {
# Best-effort dump on exit/interrupt/term.
dump_logs || true
}
trap cleanup EXIT INT TERM
echo "[container] Starting opencode as non-root..."
runuser -u opencode -- /tmp/opencode-user.sh
EOS
chmod +x "$STAGE/container-entry.sh"
docker run --rm -it \
-e "HOST_UID=$HOST_UID" \
-e "HOST_GID=$HOST_GID" \
-e "TERM=${TERM:-xterm-256color}" \
-e "LANG=${LANG:-C.UTF-8}" \
-v "$WORKDIR_HOST:/work:rw" \
-v "$STAGE/home/opencode:/home/opencode:rw" \
-v "$STAGE/shadow/opencode.json:/work/opencode.json:ro" \
-v "$STAGE/shadow/.opencode.json:/work/.opencode.json:ro" \
-v "$STAGE/work_opencode:/work/.opencode:rw" \
-v "$STAGE/container-entry.sh:/tmp/container-entry.sh:ro" \
-w /work \
"$IMAGE" bash /tmp/container-entry.sh