From 4cb1328ad64b596239f39db038b9fe1f1aed4bad Mon Sep 17 00:00:00 2001 From: Jarek Rozanski Date: Sun, 27 Sep 2026 20:49:11 +0200 Subject: [PATCH] Installer --- Makefile | 12 +++++- README.md | 23 +++++++++- scripts/install-shortcut.sh | 86 +++++++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 2 deletions(-) create mode 100755 scripts/install-shortcut.sh diff --git a/Makefile b/Makefile index d86137e..660351d 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ RUN_FLAGS := \ -v $(STATE_DIR):/home/dev/.local/state/opencode \ -w /workspace -.PHONY: build opencode shell +.PHONY: build opencode shell install # Only relevant on macOS, where podman requires a running VM ("podman # machine"). Handles "machine exists but stopped" -> start it. Does NOT @@ -67,3 +67,13 @@ shell: @$(ENSURE_MACHINE) @$(ENSURE_NETWORK) podman run $(RUN_FLAGS) $(IMAGE) bash + +# Builds the image, then installs a global `opencode-sandbox` shortcut on +# the host (see scripts/install-shortcut.sh for what "installs" means -- a +# shim in ~/.local/bin plus a PATH edit for the user's login shell). Kept out +# of the recipe itself: multi-line shell-with-conditionals in a Make recipe +# means doubling every $ as $$ and fighting Make's per-line shell splitting +# (see the ENSURE_MACHINE comment above) -- not worth it for something this +# size. +install: build + sh scripts/install-shortcut.sh "$(CURDIR)" diff --git a/README.md b/README.md index 1ecfba1..5679d7c 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,31 @@ A rootless Podman container for running [OpenCode](https://github.com/anomalyco/ make build # build (or rebuild) the container image make opencode # launch the OpenCode TUI in the current directory make shell # open a bash shell in the container instead +make install # build, then install a global `opencode-sandbox` shortcut (see below) ``` Run `make opencode` or `make shell` from any project directory — the image is shared, so there's nothing per-project to set up. +### `make install` + +Installs a shim at `~/.local/bin/opencode-sandbox` that runs `make opencode` +against this repo's Makefile, using whatever directory you're standing in — +not this repo — as `/workspace`. Once installed, `opencode-sandbox` works as +a global command from any directory, no `cd` into this repo required. (Only +`opencode` gets a shortcut; `make shell` still has to be run from here.) + +It also makes sure `~/.local/bin` is on `PATH` for future shells, based on +your login shell (`$SHELL`): + +- zsh → appended to `~/.zshrc` (or `~/.zshenv` if that's the only one of the two that already exists; `~/.zshrc` is created if neither does) +- bash → `~/.bash_profile` on macOS, `~/.bashrc` on Linux (created if missing) +- fish → added via `fish_add_path` (no config file edit needed) +- anything else → appended to `~/.profile` + +The edit is wrapped in `# >>> opencode-sandbox >>>` markers so re-running +`make install` never duplicates it, and is skipped entirely if +`~/.local/bin` is already on `PATH`. + ## How it's put together **Containerfile** — multi-stage build on `debian:trixie-slim`: @@ -37,7 +58,7 @@ Run `make opencode` or `make shell` from any project directory — the image is - OpenCode is downloaded directly from its canonical GitHub releases (`github.com/anomalyco/opencode`) for the container's architecture, and its SHA256 is checked against a hash pinned in the Containerfile before it's installed — there's no maintainer-published signature for OpenCode releases, so this hash is captured by hand and is the actual root of trust here. Bump `OPENCODE_VERSION` and re-run the `sha256sum` steps documented in the Containerfile's comments whenever you upgrade it. - A few build-time smoke tests (`ldd`, `java -version`, `node --version`, etc.) fail the build immediately if anything is broken, rather than surfacing at first `make opencode`. -**Makefile** — three targets only, as designed: +**Makefile** — four targets: - `build` builds the image. - `opencode` and `shell` both run the container with the same flags: `--userns=keep-id` (so files created in the container are owned by your real host user, not root — verified: rootless Podman's user-namespace remapping means `id` shows `root` *inside* the container's own namespace view, but the file lands on the host owned by your actual UID/GID), the current directory bind-mounted to `/workspace`, and OpenCode's three XDG directories bind-mounted from your home directory: diff --git a/scripts/install-shortcut.sh b/scripts/install-shortcut.sh new file mode 100755 index 0000000..2e556b4 --- /dev/null +++ b/scripts/install-shortcut.sh @@ -0,0 +1,86 @@ +#!/bin/sh +# Installs a global `opencode-sandbox` shim so that command, run from any +# directory on the host, runs `make opencode` against *this* repo's +# Makefile, mounting whatever directory you're standing in (not this repo) +# as /workspace. Invoked from the Makefile's `install` target as: +# sh scripts/install-shortcut.sh "$(CURDIR)" +# +# POSIX sh only (no bashisms) -- this always runs under Make's own +# $(SHELL), normally /bin/sh, regardless of the user's interactive shell. +set -eu + +REPO_DIR=${1:?"usage: $0 "} +BIN_DIR="$HOME/.local/bin" +SHIM="$BIN_DIR/opencode-sandbox" + +mkdir -p "$BIN_DIR" + +# -f (not -C): -C would chdir make into the repo before $(CURDIR) is +# evaluated, breaking the "mount whatever directory you're standing in" +# behavior that -v $(CURDIR):/workspace in the Makefile depends on. +cat > "$SHIM" < Installed shim: $SHIM" + +# Already on PATH? Nothing left to do. +case ":$PATH:" in + *":$BIN_DIR:"*) + echo "==> $BIN_DIR is already on PATH." + exit 0 + ;; +esac + +# Resolve the user's *login* shell via $SHELL, not the shell running this +# script (always Make's $(SHELL)/sh) -- same reason rustup/nvm/pnpm's own +# installers all key off $SHELL instead of process-detecting the caller. +shell_name=$(basename "${SHELL:-}") + +if [ "$shell_name" = fish ] && command -v fish >/dev/null 2>&1; then + # fish_add_path persists via a universal variable -- no config file to edit. + fish -c "fish_add_path $BIN_DIR" + echo "==> Added $BIN_DIR to PATH via fish_add_path." + exit 0 +fi + +case "$shell_name" in + zsh) + # Prefer .zshrc (the standard interactive-shell file); if it doesn't + # exist but .zshenv does, respect that existing setup instead of + # scattering PATH across two files. Only create .zshrc if neither exists. + if [ -f "$HOME/.zshrc" ]; then + rc="$HOME/.zshrc" + elif [ -f "$HOME/.zshenv" ]; then + rc="$HOME/.zshenv" + else + rc="$HOME/.zshrc" + fi + ;; + bash) + # macOS Terminal.app runs bash as a login shell, sourcing + # ~/.bash_profile (not ~/.bashrc) unless the user's profile chains to it. + case "$(uname -s)" in + Darwin) rc="$HOME/.bash_profile" ;; + *) rc="$HOME/.bashrc" ;; + esac + ;; + *) rc="$HOME/.profile" ;; +esac + +marker_begin="# >>> opencode-sandbox >>>" +marker_end="# <<< opencode-sandbox <<<" + +if [ -f "$rc" ] && grep -qF "$marker_begin" "$rc"; then + echo "==> $rc already configures PATH for opencode-sandbox; skipping." +else + touch "$rc" + { + printf '\n%s\n' "$marker_begin" + printf 'export PATH="%s:$PATH"\n' "$BIN_DIR" + printf '%s\n' "$marker_end" + } >> "$rc" + echo "==> Added $BIN_DIR to PATH in $rc." + echo "==> Restart your shell (or run: . $rc) to pick it up." +fi