-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrun-opencode.sh
More file actions
executable file
·232 lines (209 loc) · 10.2 KB
/
Copy pathrun-opencode.sh
File metadata and controls
executable file
·232 lines (209 loc) · 10.2 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
225
226
227
228
229
230
231
232
#!/usr/bin/env bash
# run-opencode.sh — launch opencode in Docker, mounting ONLY the target project dir.
# The project is mounted at the SAME absolute path inside the container as on the
# host (not remapped to /workspace) so that `-c`/--continue can match sessions by
# project path. Uses the official image (opencode binary baked in).
#
# Usage:
# ./run-opencode.sh # interactive project picker
# ./run-opencode.sh <project-name> # non-interactive, must be in ALLOWED_PROJECTS
#
# Session selection, in priority order:
# 1. OPENCODE_SESSION env var, if set -> resume that exact session
# 2. an entry for the chosen project in the session map file (see below)
# 3. OPENCODE_CONTINUE=1 -> continue the most recent session
# 4. OPENCODE_CONTINUE=0 -> start a new session
# 5. otherwise: interactive prompt (continue last vs. start new)
#
# Per-project session map:
# Kept OUT of this script (which may be public/open-sourced) in a small shell
# file that fills a PROJECT_SESSIONS[<project-name>]=<session-id> assoc array,
# e.g.:
# PROJECT_SESSIONS[PeopleModeler]="ses_0e6821896ffehruuYg2pBf9G0v"
# PROJECT_SESSIONS[TripMind]="ses_048f17cf2ffe9dEhXF0lEcpL90"
# By default this file is expected at sessions.env next to this script.
# Override its location with OPENCODE_SESSION_MAP_FILE. Add it to .gitignore.
set -euo pipefail
IMAGE_NAME="ghcr.io/falltrades/opencode-config/rust:1.1.0"
BASE_DIR="${HOME}/git/StellaSecret"
# Keep this list in sync with AGENTS.md's allowed project list.
ALLOWED_PROJECTS=(CVGenerator TripMind GameTracker AsthmeTrack PeopleModeler SmartShoppingCalculator StellaSecret.github.io gdrive-appdata-browser)
# --- resolve the directory this script lives in, regardless of cwd or alias ---
SCRIPT_DIR="$(cd "$(dirname "$(realpath "${BASH_SOURCE[0]}")")" && pwd)"
# --- per-project session mapping, kept OUT of this (public) script.
# Provide a shell file that fills PROJECT_SESSIONS[<project-name>]=<session-id>.
# Defaults to sessions.env next to this script; overridable via
# OPENCODE_SESSION_MAP_FILE. Not committed. ---
declare -A PROJECT_SESSIONS
SESSION_MAP_FILE="${OPENCODE_SESSION_MAP_FILE:-${SCRIPT_DIR}/sessions.env}"
if [[ -f "$SESSION_MAP_FILE" ]]; then
# shellcheck disable=SC1090
source "$SESSION_MAP_FILE"
fi
# --- opencode's own config/data/state/cache (your REAL host dirs, so you don't
# have to re-authenticate inside the container). Mounted read-write since
# opencode updates model choice, session state, and cache in here.
# Created here (as YOUR user) so Docker never auto-creates them as root.
#
# NOTE: .local and .cache are mounted at the PARENT level (~/.local,
# ~/.cache), not just their .../opencode leaf. That's deliberate: the
# whole point of XDG_DATA_HOME/XDG_STATE_HOME/XDG_CACHE_HOME is that any
# tool can create its own subdirectory there (opencode's own Bun runtime
# does this for ~/.local itself, dx/wasm-bindgen/npm may do it under
# ~/.cache, etc). If only the narrow .../opencode leaf were mounted,
# Docker would auto-create the ~/.local or ~/.cache parent as an empty,
# root-owned directory to hang that mount off of — writable by nobody
# but root, so any OTHER tool trying to create its own dir there hits
# EACCES. Mounting the parent is a strict superset (still contains the
# same opencode subdirs) and closes off that whole class of surprise. ---
CONFIG_DIR="${HOME}/.config/opencode"
LOCAL_DIR="${HOME}/.local"
CACHE_ROOT_DIR="${HOME}/.cache"
# --- cargo/rustup state: real host dirs, read-write. Needed for actual
# `cargo build`/`cargo install` work you do inside a project (registry
# cache, build artifacts) — NOT for the `rustup` binary itself, which
# now lives at /usr/local/bin inside the image (see Dockerfile).
#
# NOTE: ~/.cargo/bin also holds rustup's *proxy* binaries (rustc, cargo,
# rustfmt, ...). Those were generated by whatever machine first ran
# `rustup-init` against this shared CARGO_HOME — if that was ever a
# glibc host, the proxies are glibc and simply can't execute in this
# musl/Alpine container ("cannot execute: required file not found"),
# no matter what RUSTUP_TOOLCHAIN is set to. To sidestep that, PATH
# below points straight at the musl toolchain's own bin/ dir under
# RUSTUP_HOME (real binaries, not proxies) ahead of ~/.cargo/bin, so
# rustc/cargo always resolve to something that (a) can execute here
# and (b) actually has the wasm32-unknown-unknown target dx needs —
# unlike the apk-installed system rust, which can never gain targets.
# If ~/.rustup is empty (fresh machine), that PATH entry is simply
# skipped; run `rustup toolchain install stable-x86_64-unknown-linux-musl`
# once inside the container first.
#
# Created here as YOUR user so Docker never auto-creates them as root,
# and so the bind-mounted contents are writable by the UID the
# container runs as (--user "$(id -u):$(id -g)" below). ---
CARGO_HOME_DIR="${HOME}/.cargo"
RUSTUP_HOME_DIR="${HOME}/.rustup"
# --- compatibility fix: the apk-installed rustc self-reports its host
# triple as x86_64-alpine-linux-musl, but rustup names an installed
# toolchain's own component directories using the canonical
# x86_64-unknown-linux-musl triple instead. That mismatch breaks
# discovery of host-native tools under lib/rustlib/<host-triple>/bin/
# — notably rust-objcopy, which `dx build --release` needs to strip
# the compiled .wasm binary during bundling ("No such file or
# directory (os error 2)" partway through "Bundling app...").
# Symlinking the alpine-named path to the real one fixes it. Done
# here (idempotent, host-side) rather than relying on a one-off
# manual fix inside the container, so it self-heals on a fresh
# machine or if this toolchain ever gets reinstalled. ---
MUSL_RUSTLIB_DIR="${RUSTUP_HOME_DIR}/toolchains/stable-x86_64-unknown-linux-musl/lib/rustlib"
if [[ -d "${MUSL_RUSTLIB_DIR}/x86_64-unknown-linux-musl" && ! -e "${MUSL_RUSTLIB_DIR}/x86_64-alpine-linux-musl" ]]; then
ln -sf "${MUSL_RUSTLIB_DIR}/x86_64-unknown-linux-musl" "${MUSL_RUSTLIB_DIR}/x86_64-alpine-linux-musl"
fi
# --- git identity/creds, read-only (container shouldn't rewrite your git config) ---
GIT_CONFIG_FILE="${HOME}/.gitconfig"
# Global instructions file, mounted read-only so the container can't alter it.
GLOBAL_AGENTS_MD="${HOME}/.config/opencode/AGENTS.md"
# --- pick a project: arg if given, else interactive menu ---
if [[ $# -eq 0 ]]; then
echo "Select a project:" >&2
select choice in "${ALLOWED_PROJECTS[@]}"; do
if [[ -n "${choice:-}" ]]; then
PROJECT_NAME="$choice"
break
fi
echo "Invalid choice, try again." >&2
done
elif [[ $# -eq 1 ]]; then
PROJECT_NAME="$1"
else
echo "Usage: $0 [project-name]" >&2
echo "Allowed: ${ALLOWED_PROJECTS[*]}" >&2
exit 1
fi
is_allowed=false
for p in "${ALLOWED_PROJECTS[@]}"; do
if [[ "$PROJECT_NAME" == "$p" ]]; then
is_allowed=true
break
fi
done
if [[ "$is_allowed" != true ]]; then
echo "Refusing: '$PROJECT_NAME' is not in the allowed project list." >&2
echo "Allowed: ${ALLOWED_PROJECTS[*]}" >&2
exit 1
fi
# --- auto-select the session for this project unless the caller already
# pinned one via the OPENCODE_SESSION env var (explicit override wins) ---
if [[ -z "${OPENCODE_SESSION:-}" && -n "${PROJECT_SESSIONS[$PROJECT_NAME]:-}" ]]; then
OPENCODE_SESSION="${PROJECT_SESSIONS[$PROJECT_NAME]}"
echo "Using mapped session for '$PROJECT_NAME': $OPENCODE_SESSION" >&2
fi
PROJECT_PATH="${BASE_DIR}/${PROJECT_NAME}"
# Resolve to an absolute, symlink-free path and re-check it's really inside BASE_DIR.
REAL_BASE="$(realpath "$BASE_DIR")"
REAL_PROJECT="$(realpath "$PROJECT_PATH" 2>/dev/null || true)"
if [[ -z "$REAL_PROJECT" ]]; then
echo "Refusing: '$PROJECT_PATH' does not exist." >&2
exit 1
fi
case "$REAL_PROJECT" in
"$REAL_BASE"/*) ;;
*)
echo "Refusing: resolved path '$REAL_PROJECT' escapes '$REAL_BASE'." >&2
exit 1
;;
esac
# --- session handling: explicit id/flag via env skips the prompt; otherwise ask ---
SESSION_ARGS=()
if [[ -n "${OPENCODE_SESSION:-}" ]]; then
SESSION_ARGS=(-s "$OPENCODE_SESSION")
elif [[ -n "${OPENCODE_CONTINUE:-}" ]]; then
if [[ "$OPENCODE_CONTINUE" == "1" ]]; then
SESSION_ARGS=(-c)
fi
else
echo "Session for '$PROJECT_NAME':" >&2
select session_choice in "Continue last session" "Start new session"; do
case "$session_choice" in
"Continue last session") SESSION_ARGS=(-c); break ;;
"Start new session") SESSION_ARGS=(); break ;;
*) echo "Invalid choice, try again." >&2 ;;
esac
done
fi
# --- assemble mounts ---
DOCKER_ARGS=(
--rm -it
--network bridge
--cap-drop=ALL
--security-opt no-new-privileges
--user "$(id -u):$(id -g)"
--tmpfs "/tmp:rw,exec,size=2g,uid=$(id -u),gid=$(id -g),mode=1777"
-e "HOME=/home/opencode"
-w "$REAL_PROJECT"
-v "${REAL_PROJECT}:${REAL_PROJECT}"
-v "${CONFIG_DIR}:/home/opencode/.config/opencode"
-v "${LOCAL_DIR}:/home/opencode/.local"
-v "${CACHE_ROOT_DIR}:/home/opencode/.cache"
-v "${CARGO_HOME_DIR}:/home/opencode/.cargo"
-v "${RUSTUP_HOME_DIR}:/home/opencode/.rustup"
-v "/etc/passwd:/etc/passwd:ro"
-v "/etc/group:/etc/group:ro"
-e "CARGO_HOME=/home/opencode/.cargo"
-e "RUSTUP_HOME=/home/opencode/.rustup"
-e "PATH=/usr/local/sbin:/usr/local/bin:/home/opencode/.rustup/toolchains/stable-x86_64-unknown-linux-musl/bin:/home/opencode/.cargo/bin:/usr/sbin:/usr/bin:/sbin:/bin"
-e "RUSTUP_TOOLCHAIN=stable-x86_64-unknown-linux-musl"
-e "OPENCODE_API_KEY=${OPENCODE_API_KEY:-}"
)
if [[ -f "$GIT_CONFIG_FILE" ]]; then
DOCKER_ARGS+=(-v "${GIT_CONFIG_FILE}:/home/opencode/.gitconfig:ro")
else
echo "Note: no ~/.gitconfig found — skipping that mount." >&2
fi
if [[ -f "$GLOBAL_AGENTS_MD" ]]; then
DOCKER_ARGS+=(-v "${GLOBAL_AGENTS_MD}:/home/opencode/.config/opencode/AGENTS.md:ro")
else
echo "Note: no global AGENTS.md found at ${GLOBAL_AGENTS_MD} — skipping that mount." >&2
fi
docker run "${DOCKER_ARGS[@]}" "$IMAGE_NAME" "${SESSION_ARGS[@]}"