Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/opencode/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "opencode CLI",
"id": "opencode",
"version": "0.3.6",
"version": "0.3.7",
"description": "Installs the opencode AI coding agent CLI and ensures volume-mounted data directories are owned by the correct user.",
"documentationURL": "https://opencode.ai",
"options": {
Expand Down
54 changes: 54 additions & 0 deletions src/opencode/postStartCommand.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,60 @@ echo_prefix="[opencode-poststart]"

autoupdate="${AUTOUPDATE:-true}"

# ensure volume-mounted config directories exist and have correct ownership
# resolve the user and home matching where install.sh placed the config
TARGET_USER=""
TARGET_HOME="${HOME:-}"

if [ -n "$TARGET_HOME" ]; then
TARGET_USER="$(getent passwd | awk -F: -v h="$TARGET_HOME" '$6 == h {print $1; exit}')" || true
fi

if [ -z "$TARGET_USER" ]; then
user="${_REMOTE_USER:-${USERNAME:-}}"
valid_user="$(getent passwd | awk -F: '$3 >= 1000 && $1 !~ /^(nobody|nfsnobody|daemon)$/ {print $1; exit 0}')" || true
[ -z "$valid_user" ] && valid_user="root"
if [ -n "$user" ]; then
user_shell="$(getent passwd "$user" 2>/dev/null | cut -d: -f7)" || true
case "$user_shell" in
*/nologin|*/false|"")
user="$valid_user"
;;
esac
fi
[ -z "$user" ] && user="$valid_user"
TARGET_USER="$user"
TARGET_HOME="$(getent passwd "$TARGET_USER" 2>/dev/null | cut -d: -f6)"
[ -z "$TARGET_HOME" ] && TARGET_HOME="/home/$TARGET_USER"
fi

# recreate directories masked by volume mounts
mkdir -p "${TARGET_HOME}/.config/opencode"
mkdir -p "${TARGET_HOME}/.local/share/opencode"
mkdir -p "${TARGET_HOME}/.local/state/opencode"

# ensure opencode.json with lsp config exists
opencode_config="${TARGET_HOME}/.config/opencode/opencode.json"
if [ -f "$opencode_config" ]; then
if ! jq -e '.lsp' "$opencode_config" >/dev/null 2>&1; then
jq '. + {"lsp": {}}' "$opencode_config" > "${opencode_config}.tmp" && mv "${opencode_config}.tmp" "$opencode_config"
echo "$echo_prefix added 'lsp' key to existing opencode.json"
fi
else
cat > "$opencode_config" <<'EOF'
{
"$schema": "https://opencode.ai/config.json",
"lsp": {}
}
EOF
echo "$echo_prefix created opencode.json with LSP enabled"
fi

chown -R "${TARGET_USER}:${TARGET_USER}" \
"${TARGET_HOME}/.config/opencode" \
"${TARGET_HOME}/.local/share/opencode" \
"${TARGET_HOME}/.local/state/opencode" 2>/dev/null || true

if ! command -v opencode >/dev/null 2>&1; then
echo "$echo_prefix opencode not found, installing..."
curl -fsSL https://opencode.ai/install | bash
Expand Down
8 changes: 8 additions & 0 deletions test/opencode/autoupdate_disabled.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,12 @@ check "opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG"

check "opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG")" -eq 0

# test postStartCommand recreates config when missing (volume mount case)
OPENCODE_CONFIG_BAK="$OPENCODE_CONFIG"
rm -f "$OPENCODE_CONFIG_BAK"
AUTOUPDATE=false bash /usr/local/share/devcontainer-features/opencode-postStartCommand.sh || true
check "postStartCommand recreates opencode.json" test -f "$OPENCODE_CONFIG_BAK"
check "recreated opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG_BAK"
check "recreated opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG_BAK")" -eq 0

reportResults
8 changes: 8 additions & 0 deletions test/opencode/autoupdate_enabled.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,12 @@ check "opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG"

check "opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG")" -eq 0

# test postStartCommand recreates config when missing (volume mount case)
OPENCODE_CONFIG_BAK="$OPENCODE_CONFIG"
rm -f "$OPENCODE_CONFIG_BAK"
AUTOUPDATE=false bash /usr/local/share/devcontainer-features/opencode-postStartCommand.sh || true
check "postStartCommand recreates opencode.json" test -f "$OPENCODE_CONFIG_BAK"
check "recreated opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG_BAK"
check "recreated opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG_BAK")" -eq 0

reportResults
8 changes: 8 additions & 0 deletions test/opencode/custom_username.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,12 @@ check "opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG"

check "opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG")" -eq 0

# test postStartCommand recreates config when missing (volume mount case)
OPENCODE_CONFIG_BAK="$OPENCODE_CONFIG"
rm -f "$OPENCODE_CONFIG_BAK"
AUTOUPDATE=false bash /usr/local/share/devcontainer-features/opencode-postStartCommand.sh || true
check "postStartCommand recreates opencode.json" test -f "$OPENCODE_CONFIG_BAK"
check "recreated opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG_BAK"
check "recreated opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG_BAK")" -eq 0

reportResults
8 changes: 8 additions & 0 deletions test/opencode/default.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,12 @@ check "opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG"

check "opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG")" -eq 0

# test postStartCommand recreates config when missing (volume mount case)
OPENCODE_CONFIG_BAK="$OPENCODE_CONFIG"
rm -f "$OPENCODE_CONFIG_BAK"
AUTOUPDATE=false bash /usr/local/share/devcontainer-features/opencode-postStartCommand.sh || true
check "postStartCommand recreates opencode.json" test -f "$OPENCODE_CONFIG_BAK"
check "recreated opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG_BAK"
check "recreated opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG_BAK")" -eq 0

reportResults
8 changes: 8 additions & 0 deletions test/opencode/specific_version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,12 @@ check "opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG"

check "opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG")" -eq 0

# test postStartCommand recreates config when missing (volume mount case)
OPENCODE_CONFIG_BAK="$OPENCODE_CONFIG"
rm -f "$OPENCODE_CONFIG_BAK"
AUTOUPDATE=false bash /usr/local/share/devcontainer-features/opencode-postStartCommand.sh || true
check "postStartCommand recreates opencode.json" test -f "$OPENCODE_CONFIG_BAK"
check "recreated opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG_BAK"
check "recreated opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG_BAK")" -eq 0

reportResults
8 changes: 8 additions & 0 deletions test/opencode/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,12 @@ check "opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG"

check "opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG")" -eq 0

# test postStartCommand recreates config when missing (volume mount case)
OPENCODE_CONFIG_BAK="$OPENCODE_CONFIG"
rm -f "$OPENCODE_CONFIG_BAK"
AUTOUPDATE=false bash /usr/local/share/devcontainer-features/opencode-postStartCommand.sh || true
check "postStartCommand recreates opencode.json" test -f "$OPENCODE_CONFIG_BAK"
check "recreated opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG_BAK"
check "recreated opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG_BAK")" -eq 0

reportResults
7 changes: 7 additions & 0 deletions wiki/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,15 @@
└── validate.yaml # Validate features
src/ # Feature implementations
├── opencode/
│ ├── devcontainer-feature.json # metadata + options
│ ├── install.sh # build-time setup
│ ├── postStartCommand.sh # runtime config (volume resilience)
│ ├── README.md
│ └── NOTES.md
├── agents-workspace/
│ ├── devcontainer-feature.json
│ ├── install.sh
│ ├── postStartCommand.sh
│ ├── README.md
│ └── NOTES.md
└── agency-agents/
Expand Down
9 changes: 8 additions & 1 deletion wiki/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@

Each feature lives in `src/<feature>/` with:
- `devcontainer-feature.json` — metadata + options
- `install.sh` — entrypoint script
- `install.sh` — entrypoint script (build time)
- `postStartCommand.sh` — post-start script (runtime)

## Volume Mount Resilience

Config files and directories that must survive Docker volume mounts must be created or ensured in `postStartCommand.sh`, not only in `install.sh`. Volumes mounted at runtime replace build-time directories; `postStartCommand.sh` runs after volumes are mounted and can recreate any missing state.

`install.sh` may still create these files at build time for the non-volume case, but `postStartCommand.sh` must be the authoritative source for runtime state.

## Options

Expand Down
22 changes: 22 additions & 0 deletions wiki/decisions/poststart-config-creation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
title: PostStart config creation for volume mount resilience
date: 2026-05-26
status: accepted
---

## Context

The `opencode` feature creates `~/.config/opencode/opencode.json` (with `lsp: {}`) during `install.sh` at build time. When a Docker volume is mounted at `/home/vscode/.config/opencode`, the volume starts empty at runtime and hides the build-time file. The `postStartCommand.sh` did not recreate it, so LSP servers remained unconfigured.

The `agents-workspace` feature demonstrates the correct pattern: `install.sh` only copies the postStart script, and all actual setup happens in `postStartCommand.sh` at runtime.

## Decision

Move `opencode.json` creation (and directory ownership fixes) from `install.sh` into `postStartCommand.sh`, which runs every container start. `install.sh` still creates the file at build time (harmless for non-volume cases), but `postStartCommand.sh` guarantees it exists even when a volume replaces the directory.

## Consequences

- Volume mounts no longer break LSP server configuration
- `postStartCommand.sh` is idempotent — if the file exists with `lsp`, it skips creation; if it exists without `lsp`, it merges it in
- Ownership is corrected at runtime in case the volume changes UID/GID mappings
- Tests now validate that `postStartCommand.sh` can recreate the config independently
6 changes: 5 additions & 1 deletion wiki/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@

## opencode

Installs opencode CLI and fixes volume-mounted directory permissions.
Installs opencode CLI and ensures volume-mounted config directories are writable.

Options:
- `username` (default: _REMOTE_USER) — user to install for
- `version` (default: empty/latest) — install specific version
- `autoupdate` (default: true) — auto-upgrade on container start

Behavior:
- `install.sh` installs the CLI, creates directories, writes `~/.config/opencode/opencode.json` with `lsp: {}`, and generates a fix-permissions script.
- `postStartCommand.sh` ensures `opencode.json` exists at every start (critical when `~/.config/opencode` is a volume mount), installing opencode if missing and running autoupgrade when enabled.

Idempotency: marker uses version if specified, else "latest"

## agency-agents
Expand Down
3 changes: 2 additions & 1 deletion wiki/index.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# Wiki

- [architecture.md](architecture.md) — project structure and feature layout
- [conventions.md](conventions.md) — feature structure and option patterns
- [conventions.md](conventions.md) — feature structure, option patterns, volume mount resilience
- [features.md](features.md) — available features and their purpose
- [publishing.md](publishing.md) — release workflow and GHCR publishing
- [testing.md](testing.md) — testing commands and CLI usage
- [decisions/poststart-config-creation.md](decisions/poststart-config-creation.md) — runtime config creation for volume mount support

## Operations

Expand Down
Loading