-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathocd
More file actions
executable file
·224 lines (205 loc) · 8.48 KB
/
ocd
File metadata and controls
executable file
·224 lines (205 loc) · 8.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
#!/usr/bin/env bash
set -e
# Get the actual script location (resolving symlinks)
SOURCE="${BASH_SOURCE[0]}"
while [ -h "$SOURCE" ]; do
SCRIPT_DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
SOURCE="$(readlink "$SOURCE")"
[[ $SOURCE != /* ]] && SOURCE="$SCRIPT_DIR/$SOURCE"
done
SCRIPT_DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
# Get the current working directory (where script is run from)
CURRENT_DIR="$(pwd -P)"
WORKSPACE_BASENAME="$(basename "$CURRENT_DIR")"
WORKSPACE_BASENAME="${WORKSPACE_BASENAME//[^A-Za-z0-9._-]/-}"
WORKSPACE_BASENAME="${WORKSPACE_BASENAME#-}"
WORKSPACE_BASENAME="${WORKSPACE_BASENAME%-}"
if [ -z "$WORKSPACE_BASENAME" ]; then
WORKSPACE_BASENAME="workspace"
fi
if command -v sha256sum >/dev/null 2>&1; then
CURRENT_DIR_HASH="$(printf '%s' "$CURRENT_DIR" | sha256sum | cut -c1-12)"
elif command -v shasum >/dev/null 2>&1; then
CURRENT_DIR_HASH="$(printf '%s' "$CURRENT_DIR" | shasum -a 256 | cut -c1-12)"
else
echo "Error: neither sha256sum nor shasum is available to hash the workspace path."
exit 1
fi
CONTAINER_WORKSPACE="/workspaces/${WORKSPACE_BASENAME}-${CURRENT_DIR_HASH}"
# Parse flags (must come before any Docker args)
OMO_PROFILE=""
WEB_MODE=false
TMUX_MODE=false
WEB_PORT="${OCD_WEB_PORT:-4096}"
DOCKER_ARGS=()
while [[ $# -gt 0 ]]; do
case "$1" in
--profile)
if [[ -n "${2:-}" ]]; then
OMO_PROFILE="$2"
shift 2
else
echo "Error: --profile requires a value (e.g., --profile minimax)"
exit 1
fi
;;
--web)
WEB_MODE=true
shift
;;
--tmux)
TMUX_MODE=true
shift
;;
--port)
if [[ -n "${2:-}" ]]; then
WEB_PORT="$2"
shift 2
else
echo "Error: --port requires a value (e.g., --port 4096)"
exit 1
fi
;;
*)
DOCKER_ARGS+=("$1")
shift
;;
esac
done
set -- "${DOCKER_ARGS[@]}"
# Configuration
IMAGE_NAME="ocd:latest"
CONTAINER_NAME_PREFIX="${OCD_CONTAINER_NAME_PREFIX:-ocd}"
CONTAINER_NAME="${CONTAINER_NAME_PREFIX}-$(date +%Y%m%d-%H%M%S)-$$"
# Check if image exists
if ! docker image inspect "${IMAGE_NAME}" >/dev/null 2>&1; then
echo "Error: Image '${IMAGE_NAME}' not found."
echo "Please run ./build first to build the image."
exit 1
fi
# Config: merge local overrides on top of base config if they exist.
# Objects are merged recursively; arrays are appended with duplicate entries skipped.
JQ_MERGE_FILTER='def merge_arrays($a; $b): reduce $b[] as $item ($a; if index($item) == null then . + [$item] else . end); def deepmerge($a; $b): reduce ($b | keys_unsorted[]) as $key ($a; .[$key] = if (($a[$key] | type) == "object" and ($b[$key] | type) == "object") then deepmerge($a[$key]; $b[$key]) elif (($a[$key] | type) == "array" and ($b[$key] | type) == "array") then merge_arrays($a[$key]; $b[$key]) else $b[$key] end); deepmerge(.[0]; .[1])'
OPENCODE_CONFIG_HOST="${SCRIPT_DIR}/config/opencode.json"
OPENCODE_QUOTA_CONFIG_HOST="${SCRIPT_DIR}/config/opencode-quota/quota-toast.json"
TUI_CONFIG_HOST="${SCRIPT_DIR}/config/tui.json"
TMUX_CONFIG_HOST="${SCRIPT_DIR}/config/tmux.conf"
if [ -f "${SCRIPT_DIR}/config/opencode.local.json" ]; then
echo "Merging opencode.local.json into opencode.json..."
jq -s "$JQ_MERGE_FILTER" \
"${SCRIPT_DIR}/config/opencode.json" \
"${SCRIPT_DIR}/config/opencode.local.json" \
> "${SCRIPT_DIR}/config/opencode.merged.json"
OPENCODE_CONFIG_HOST="${SCRIPT_DIR}/config/opencode.merged.json"
fi
# oh-my-openagent config: select base config based on --profile flag.
# Available profiles are files named oh-my-openagent.<profile>.json in config/.
# Default (no --profile) uses oh-my-openagent.json.
# NOTE: tui.json is NOT part of the merge flow — it is read directly by OpenCode from the config dir.
if [ -n "$OMO_PROFILE" ]; then
OMO_BASE_CONFIG="${SCRIPT_DIR}/config/oh-my-openagent.${OMO_PROFILE}.json"
if [ ! -f "$OMO_BASE_CONFIG" ]; then
echo "Error: Profile '${OMO_PROFILE}' not found (expected ${OMO_BASE_CONFIG})"
echo "Available profiles:"
for f in "${SCRIPT_DIR}"/config/oh-my-openagent.*.json; do
[ -f "$f" ] || continue
basename "$f" | sed 's/^oh-my-openagent\.//; s/\.json$//' | grep -v '^local$\|^merged$'
done
exit 1
fi
echo "Using oh-my-openagent profile: ${OMO_PROFILE}"
else
OMO_BASE_CONFIG="${SCRIPT_DIR}/config/oh-my-openagent.json"
fi
OMO_CONFIG_HOST="$OMO_BASE_CONFIG"
if [ -f "${SCRIPT_DIR}/config/oh-my-openagent.local.json" ]; then
echo "Merging oh-my-openagent.local.json into $(basename "$OMO_BASE_CONFIG")..."
jq -s "$JQ_MERGE_FILTER" \
"$OMO_BASE_CONFIG" \
"${SCRIPT_DIR}/config/oh-my-openagent.local.json" \
> "${SCRIPT_DIR}/config/oh-my-openagent.merged.json"
OMO_CONFIG_HOST="${SCRIPT_DIR}/config/oh-my-openagent.merged.json"
fi
# X11 clipboard support: mount the socket dir only if it exists
X11_MOUNT=()
if [ -d /tmp/.X11-unix ]; then
X11_MOUNT=(-v /tmp/.X11-unix:/tmp/.X11-unix:ro)
fi
# Custom OpenCode agents: Markdown files in config/agents are loaded via OPENCODE_CONFIG_DIR.
AGENTS_MOUNT=()
if [ -d "${SCRIPT_DIR}/config/agents" ]; then
AGENTS_MOUNT=(-v "${SCRIPT_DIR}/config/agents:/config/agents:ro")
fi
# Configure command (network is always --network host for Ollama compatibility)
if [ "$WEB_MODE" = true ] && [ "$TMUX_MODE" = true ]; then
echo "Error: --tmux cannot be combined with --web."
exit 1
elif [ "$WEB_MODE" = true ]; then
# Auto-detect available port if the requested one is in use
while ss -tln 2>/dev/null | grep -q ":${WEB_PORT}\b" \
|| docker ps --format '{{.Ports}}' 2>/dev/null | grep -q ":${WEB_PORT}->"; do
echo "Port ${WEB_PORT} is in use, trying $((WEB_PORT + 1))..."
WEB_PORT=$((WEB_PORT + 1))
done
OPENCODE_CMD=(opencode web --hostname 0.0.0.0 --port "${WEB_PORT}")
if [ -z "${OPENCODE_SERVER_PASSWORD:-}" ]; then
echo "Warning: No OPENCODE_SERVER_PASSWORD set. Web UI will be unauthenticated."
echo " Set it via: OPENCODE_SERVER_PASSWORD=secret ./ocd --web"
fi
elif [ "$TMUX_MODE" = true ]; then
printf -v TMUX_SHELL_COMMAND 'opencode --port %q %q' "${WEB_PORT}" "${CONTAINER_WORKSPACE}"
for arg in "$@"; do
printf -v QUOTED_ARG '%q' "$arg"
TMUX_SHELL_COMMAND+=" ${QUOTED_ARG}"
done
OPENCODE_CMD=(tmux new-session -s opencode "export TMUX_PANE=\$(tmux display-message -p '#{pane_id}'); exec ${TMUX_SHELL_COMMAND}")
else
OPENCODE_CMD=(opencode "$@")
fi
SERVER_ENV=()
if [ -n "${OPENCODE_SERVER_PASSWORD:-}" ]; then
SERVER_ENV+=(-e OPENCODE_SERVER_PASSWORD="${OPENCODE_SERVER_PASSWORD}")
fi
if [ -n "${OPENCODE_SERVER_USERNAME:-}" ]; then
SERVER_ENV+=(-e OPENCODE_SERVER_USERNAME="${OPENCODE_SERVER_USERNAME}")
fi
# Run the container
if [ "$WEB_MODE" = true ]; then
echo "Starting web UI on http://localhost:${WEB_PORT} (container '${CONTAINER_NAME}', workspace '${CONTAINER_WORKSPACE}')"
elif [ "$TMUX_MODE" = true ]; then
echo "Starting tmux-backed container '${CONTAINER_NAME}' using ${CURRENT_DIR} as workspace at '${CONTAINER_WORKSPACE}'."
else
echo "Starting container '${CONTAINER_NAME}' using ${CURRENT_DIR} as workspace at '${CONTAINER_WORKSPACE}'."
fi
docker run -it \
--rm \
--entrypoint "" \
--name "${CONTAINER_NAME}" \
--network host \
-v "${SCRIPT_DIR}/data:/home/coder:rw" \
-v "${OPENCODE_CONFIG_HOST}:/config/opencode.json:ro" \
-v "${OPENCODE_QUOTA_CONFIG_HOST}:/config/opencode-quota/quota-toast.json:ro" \
-v "${OMO_CONFIG_HOST}:/config/oh-my-openagent.json:ro" \
-v "${TUI_CONFIG_HOST}:/config/tui.json:ro" \
-v "${TMUX_CONFIG_HOST}:/home/coder/.tmux.conf:ro" \
"${AGENTS_MOUNT[@]}" \
-v "${CURRENT_DIR}:${CONTAINER_WORKSPACE}:rw" \
"${X11_MOUNT[@]}" \
-w "${CONTAINER_WORKSPACE}" \
-e HOME=/home/coder \
-e PATH="/home/coder/.local/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin" \
-e DISPLAY="${DISPLAY:-}" \
-e TERM=xterm-256color \
-e LANG=C.UTF-8 \
-e LC_ALL=C.UTF-8 \
-e OPENCODE_API_KEY="${OPENCODE_API_KEY:-}" \
-e OPENCODE_CONFIG=/config/opencode.json \
-e OPENCODE_CONFIG_DIR=/config \
"${SERVER_ENV[@]}" \
--security-opt no-new-privileges:true \
--cap-drop ALL \
--cap-add CHOWN \
--cap-add SETUID \
--cap-add SETGID \
"${IMAGE_NAME}" \
"${OPENCODE_CMD[@]}"