@@ -54,12 +54,20 @@ if [[ ! -f "$DOCKERFILE" ]]; then
5454 exit 1
5555fi
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.
5860NEEDS_BUILD=false
5961if ! 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
6371fi
6472
6573if $NEEDS_BUILD ; then
8694if [[ -n " ${OPENAI_API_KEY:- } " ]]; then
8795 AGENT_ENV+=(-e " OPENAI_API_KEY=${OPENAI_API_KEY} " )
8896fi
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"
131142fi
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+
133206docker 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