Generate opencode's config from a tracked template, with matching commands, agents and pwswitch

This commit is contained in:
Gabriel Brown committed 2026-10-30 13:44:29 -04:00
1 parent 68c7d6a3ea
commit a987cbab66
16 files changed
+531 -25

No files matched your search

+59 -21
View File
@@ -23,10 +23,13 @@
# failure mode than leaving two lines for them to paste once.
#
# opencode has no CLI for this either, but its config is plain JSON, so this
# stage merges two keys into it with jq: one mcp entry per server row, and the
# rules directory under instructions, because opencode reads ~/.claude/CLAUDE.md
# and ~/.claude/skills on its own but not ~/.claude/rules. Every other key in
# that file is left as it was.
# stage writes it whole from user/agents/opencode/config.json -- the tracked
# template carrying everything that is not a secret -- plus the MCP block built
# here from the server rows. The whole file is generated rather than merged into
# because the point of the template is that every machine ends up with the same
# opencode experience, and a merged file drifts the moment one machine is edited
# by hand. The output says so at the top, and anything worth changing goes in
# the template, where it reaches the other machines too.
set -euo pipefail
@@ -37,6 +40,8 @@ PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
MCP_DIR="$PANAMA_PATH/user/agents/mcp"
SERVERS="$MCP_DIR/servers"
ENV_FILE="$MCP_DIR/env"
TEMPLATE="$PANAMA_PATH/user/agents/opencode/config.json"
PANAMA_OLD="$PANAMA_PATH/config/old"
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/panama"
DECISION="$STATE_DIR/user-content"
@@ -177,29 +182,62 @@ fi
$has_opencode || exit 0
# opencode reads opencode.jsonc or opencode.json, whichever this machine has.
# The file holds tokens once this runs, so it is written private and swapped in
# whole, the way ~/.claude.json already holds them for Claude Code.
# opencode reads opencode.jsonc. The file holds tokens once this runs, so it is
# written private and swapped in whole, the way ~/.claude.json already holds
# them for Claude Code.
OPENCODE_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/opencode"
opencode_config="$OPENCODE_DIR/opencode.jsonc"
[[ -e "$opencode_config" ]] || opencode_config="$OPENCODE_DIR/opencode.json"
rules="$HOME/.claude/rules/*.md"
mkdir -p "$OPENCODE_DIR"
current='{"$schema": "https://opencode.ai/config.json"}'
[[ -e "$opencode_config" ]] && current="$(cat "$opencode_config")"
# jq cannot read JSONC comments. A file that has them is left alone rather
# than rewritten without them.
if ! merged="$(jq --argjson mcp "$opencode_mcp" --arg rules "$rules" \
'.mcp = ((.mcp // {}) + $mcp) | .instructions = (((.instructions // []) - [$rules]) + [$rules])' \
<<<"$current" 2>/dev/null)"; then
warn "Could not parse $opencode_config as JSON; opencode MCP servers not written."
[[ -r "$TEMPLATE" ]] || { warn "No $TEMPLATE; opencode config not written."; exit 0; }
if ! jq -e . "$TEMPLATE" >/dev/null 2>&1; then
warn "$TEMPLATE is not valid JSON; opencode config not written."
exit 0
fi
# Nothing at the destination is destroyed, the promise link-user and link-skills
# make. A file this stage did not write is somebody's own work and goes to
# config/old/ under a name that says where it came from. One carrying the marker
# was written by an earlier run and is replaced in place, so re-running never
# buries its own output and config/old/ does not grow on every upgrade.
MARKER='generated by setup/scripts/link-mcp'
displace() {
local destination="$1" backup
[[ -e "$destination" || -L "$destination" ]] || return 0
if [[ -L "$destination" ]]; then
rm -f "$destination"
return 0
fi
grep -qF "$MARKER" "$destination" 2>/dev/null && return 0
backup="$PANAMA_OLD/mcp-$(printf '%s' "${destination#"$HOME"/}" | tr '/' '-')"
[[ -e "$backup" ]] && backup="$backup.$(date +%s)"
mv "$destination" "$backup"
log "Moved existing $destination to $backup"
}
mkdir -p "$OPENCODE_DIR" "$PANAMA_OLD"
# Two names for one file would leave opencode choosing between them, and the one
# this stage does not write would win or lose on discovery order.
displace "${opencode_config%.jsonc}.json"
displace "$opencode_config"
# The template is portable prose: it names the rules directory as ~/ so it does
# not carry this machine's login. opencode resolves a leading ~/ on its own, but
# expanding it here keeps the generated file readable the way a person would
# have written it by hand when they went looking for where the path came from.
merged="$(jq --argjson mcp "$opencode_mcp" --arg home "$HOME" \
'.mcp = $mcp
| .instructions = [(.instructions // [])[]
| if startswith("~/") then $home + .[1:] else . end]' \
<<<"$(cat "$TEMPLATE")")"
tmp="$(mktemp "$OPENCODE_DIR/.opencode-config.XXXXXX")"
{
printf '// %s\n' "$MARKER"
printf '// Edit user/agents/opencode/config.json in the Panama checkout instead.\n'
printf '// This file is replaced whenever ./install or panama update runs.\n'
printf '%s\n' "$merged"
} >"$tmp"
chmod 600 "$tmp"
printf '%s\n' "$merged" >"$tmp"
mv "$tmp" "$opencode_config"
log "opencode: $(jq -r 'length' <<<"$opencode_mcp") MCP servers and the rules written to $opencode_config."
log "opencode: $(jq -r 'length' <<<"$opencode_mcp") MCP servers and the template written to $opencode_config."