Skip to content

Commit b780a7e

Browse files
authored
Merge pull request #20 from adzerk/add-rlwrap
Add rlwrap installation to orchard
2 parents 4d8696a + dcdb508 commit b780a7e

3 files changed

Lines changed: 89 additions & 5 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ recording.jfr
3737
# OS X
3838
.DS_Store
3939

40+
# Orchard (generated, local-only)
41+
orchard.code-workspace
42+
4043
# Jekyll / GitHub Pages
4144
docs/_site/
4245
docs/.jekyll-cache/

Dockerfile.orchard

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
2424
git \
2525
make \
2626
vim \
27+
rlwrap \
2728
ca-certificates \
2829
gnupg \
2930
unzip \
@@ -224,16 +225,19 @@ RUN useradd -m -s /bin/bash orchard \
224225
&& mkdir -p /home/orchard/.cache \
225226
&& mkdir -p /home/orchard/.config \
226227
&& mkdir -p /home/orchard/.m2 \
228+
&& mkdir -p /home/orchard/.claude \
229+
&& mkdir -p /repos \
227230
&& cp -r /root/.m2/repository /home/orchard/.m2/repository \
228-
&& chown -R orchard:orchard /home/orchard
231+
&& chown -R orchard:orchard /home/orchard \
232+
&& chown orchard:orchard /repos
229233
USER orchard
230234
WORKDIR /workspace
231235

232236
# Set JAVA_HOME dynamically and launch bash
233237
COPY --chmod=755 <<'ENTRYPOINT' /usr/local/bin/orchard-entry.sh
234238
#!/bin/bash
235239
export JAVA_HOME=$(dirname $(dirname $(readlink -f $(which java))))
236-
export PATH="${JAVA_HOME}/bin:${PATH}"
240+
export PATH="${JAVA_HOME}/bin:${HOME}/.local/bin:${PATH}"
237241

238242
# Detect JDK 25 for Fray (the mvn wrapper references FRAY_JDK_HOME)
239243
for d in /usr/lib/jvm/temurin-25-*; do

orchard.sh

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,20 @@ if [[ ! -f "$DOCKERFILE" ]]; then
5454
exit 1
5555
fi
5656

57-
# Rebuild if image doesn't exist or Dockerfile is newer than image
57+
# Rebuild if image doesn't exist or Dockerfile is newer than image.
58+
# -nt can't compare a file against a Docker timestamp string, so we extract
59+
# epoch seconds from both sides and compare numerically.
5860
NEEDS_BUILD=false
5961
if ! docker image inspect "$IMAGE_NAME" &>/dev/null 2>&1; then
6062
NEEDS_BUILD=true
61-
elif [[ "$DOCKERFILE" -nt "$(docker image inspect "$IMAGE_NAME" --format '{{.Created}}' 2>/dev/null || echo '2000-01-01')" ]]; then
62-
NEEDS_BUILD=true
63+
else
64+
DOCKERFILE_MTIME=$(stat -f %m "$DOCKERFILE")
65+
IMAGE_CREATED=$(docker image inspect "$IMAGE_NAME" --format '{{.Created}}' 2>/dev/null)
66+
# Strip fractional seconds and timezone suffix (handles both "…Z" and "….nnnZ" forms)
67+
IMAGE_MTIME=$(date -j -f "%Y-%m-%dT%H:%M:%S" "${IMAGE_CREATED%%[.Z]*}" "+%s" 2>/dev/null || echo 0)
68+
if [[ "$DOCKERFILE_MTIME" -gt "$IMAGE_MTIME" ]]; then
69+
NEEDS_BUILD=true
70+
fi
6371
fi
6472

6573
if $NEEDS_BUILD; then
@@ -86,6 +94,9 @@ fi
8694
if [[ -n "${OPENAI_API_KEY:-}" ]]; then
8795
AGENT_ENV+=(-e "OPENAI_API_KEY=${OPENAI_API_KEY}")
8896
fi
97+
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
98+
AGENT_ENV+=(-e "GITHUB_TOKEN=${GITHUB_TOKEN}")
99+
fi
89100

90101
# Extract Claude Code OAuth credentials from macOS Keychain and pass them to the
91102
# container via a temp file (not a -e env var, which would be visible in docker inspect).
@@ -130,6 +141,68 @@ if [[ -z "${OPENAI_API_KEY:-}" ]] && [[ -f "${HOME}/.codex/auth.json" ]]; then
130141
info "Mounting Codex auth from ~/.codex/auth.json"
131142
fi
132143

144+
# ── Per-project persistent Claude volume ─────────────────────────────────────
145+
# Each PWD gets its own named volume so ~/.claude (chat history, settings) is
146+
# preserved across container sessions and compartmentalized per project.
147+
# Volume name: orchard-claude-<basename>-<8-char hash of full path>
148+
_vol_suffix=$(printf '%s' "$PROJECT_DIR" | md5 -q | cut -c1-8)
149+
CLAUDE_VOLUME="orchard-claude-$(basename "$PROJECT_DIR")-${_vol_suffix}"
150+
unset _vol_suffix
151+
if ! docker volume inspect "$CLAUDE_VOLUME" &>/dev/null 2>&1; then
152+
docker volume create "$CLAUDE_VOLUME" > /dev/null
153+
# Docker creates new volume mount points as root:root. Fix ownership so the
154+
# orchard user can write into it without needing elevated capabilities.
155+
docker run --rm \
156+
-v "${CLAUDE_VOLUME}:/home/orchard/.claude" \
157+
--user root \
158+
--entrypoint "" \
159+
"$IMAGE_NAME" \
160+
chown orchard:orchard /home/orchard/.claude
161+
info "Created persistent Claude volume: ${CLAUDE_VOLUME}"
162+
else
163+
info "Using existing Claude volume: ${CLAUDE_VOLUME}"
164+
fi
165+
166+
# Extra bind mounts injected by callers (e.g. orchardw.sh).
167+
# ORCHARD_EXTRA_MOUNTS: newline-separated list of "host:container" pairs.
168+
# Bare host paths (no colon) fall back to /repos/<basename>.
169+
EXTRA_MOUNTS=()
170+
EXTRA_CONTAINER_PATHS=()
171+
if [[ -n "${ORCHARD_EXTRA_MOUNTS:-}" ]]; then
172+
while IFS= read -r _pair; do
173+
[[ -z "$_pair" ]] && continue
174+
_host="${_pair%%:*}"
175+
_container="${_pair#*:}"
176+
[[ "$_container" == "$_pair" ]] && _container="/repos/$(basename "$_host")"
177+
if [[ -d "$_host" ]]; then
178+
EXTRA_MOUNTS+=(-v "${_host}:${_container}")
179+
EXTRA_CONTAINER_PATHS+=("$_container")
180+
fi
181+
done <<< "$ORCHARD_EXTRA_MOUNTS"
182+
fi
183+
184+
# Generate orchard.code-workspace when extra repos are mounted so VS Code
185+
# opens all roots automatically via "Dev Containers: Attach to Running Container".
186+
WORKSPACE_FILE="${PROJECT_DIR}/orchard.code-workspace"
187+
if [[ ${#EXTRA_CONTAINER_PATHS[@]} -gt 0 ]]; then
188+
{
189+
printf '{\n "folders": [\n { "path": "/workspace" },\n { "path": "/repos" }\n ]\n}\n'
190+
} > "$WORKSPACE_FILE"
191+
info "Generated orchard.code-workspace with /repos root"
192+
# Keep the generated file out of git
193+
_GITIGNORE="${PROJECT_DIR}/.gitignore"
194+
if [[ -f "$_GITIGNORE" ]] && ! grep -qxF 'orchard.code-workspace' "$_GITIGNORE"; then
195+
echo 'orchard.code-workspace' >> "$_GITIGNORE"
196+
info "Added orchard.code-workspace to .gitignore"
197+
elif [[ ! -f "$_GITIGNORE" ]]; then
198+
echo 'orchard.code-workspace' > "$_GITIGNORE"
199+
fi
200+
unset _GITIGNORE
201+
else
202+
[[ -f "$WORKSPACE_FILE" ]] && rm -f "$WORKSPACE_FILE"
203+
fi
204+
unset WORKSPACE_FILE
205+
133206
docker run \
134207
--rm \
135208
-it \
@@ -138,6 +211,8 @@ docker run \
138211
-v "${PROJECT_DIR}:/workspace" \
139212
${CLAUDE_CONFIG_MOUNT[@]+"${CLAUDE_CONFIG_MOUNT[@]}"} \
140213
${CODEX_AUTH_MOUNT[@]+"${CODEX_AUTH_MOUNT[@]}"} \
214+
${EXTRA_MOUNTS[@]+"${EXTRA_MOUNTS[@]}"} \
215+
-v "${CLAUDE_VOLUME}:/home/orchard/.claude" \
141216
${CREDS_ENV_FILE:+--env-file "$CREDS_ENV_FILE"} \
142217
--tmpfs /workspace/tooling/jdk-21.0.7+6:exec,uid=1000,gid=1000 \
143218
--tmpfs /workspace/tooling/openjml:exec,uid=1000,gid=1000 \
@@ -147,5 +222,7 @@ docker run \
147222
--cap-add DAC_OVERRIDE \
148223
--cap-add FOWNER \
149224
${AGENT_ENV[@]+"${AGENT_ENV[@]}"} \
225+
-e "ORCHARD_PROJECT=$(basename "$PROJECT_DIR")" \
226+
-e 'PROMPT_COMMAND=PS1="(\[\033[1;32m\]\u@\h\[\033[0m\])[\[\033[1;34m\]${ORCHARD_PROJECT}\[\033[0m\]] \w\$ "' \
150227
"$IMAGE_NAME" \
151228
"${@:-bash}"

0 commit comments

Comments
 (0)