mirror of
https://github.com/gnekt/My-Brain-Is-Full-Crew.git
synced 2026-09-07 22:36:26 +00:00
Implement preserving config merge for opencode.
Co-Authored-By: win0na <winnie@winneon.moe> feat(opencode): config-merge.sh with formatting-preserving JSON merge Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> build(opencode): source config-merge.sh from adapter Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> feat(install): use oc_config_merge for opencode.json instead of overwrite Source config-merge.sh from install scripts for opencode platform so user keys in opencode.json are preserved on reinstall and update. Fix in-place merge by writing to a temp file before moving to output. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,9 @@
|
||||
# opencode expects in the user's vault.
|
||||
# =============================================================================
|
||||
|
||||
# shellcheck source=adapters/opencode/config-merge.sh
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/config-merge.sh"
|
||||
|
||||
OC_PLATFORM="opencode"
|
||||
OC_FW_DIR="opencode"
|
||||
OC_DISPATCHER="AGENTS.md"
|
||||
|
||||
75
adapters/opencode/config-merge.sh
Executable file
75
adapters/opencode/config-merge.sh
Executable file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# adapters/opencode/config-merge.sh — JSON config merge for opencode.json
|
||||
# =============================================================================
|
||||
# Merges a built opencode.json with an existing one in the vault, preserving
|
||||
# user keys and formatting. Only the "mcp" key is managed; everything else
|
||||
# belongs to the user.
|
||||
#
|
||||
# Sourced by the opencode adapter. Requires jq.
|
||||
# =============================================================================
|
||||
|
||||
# Provide a fallback warn() if scripts/lib.sh has not been sourced yet.
|
||||
if ! declare -f warn >/dev/null 2>&1; then
|
||||
warn() { echo " ! $*" >&2; }
|
||||
fi
|
||||
|
||||
# oc_detect_indent <file>
|
||||
# Detects the indentation unit used in a JSON file.
|
||||
# Returns the number of spaces (2 or 4). Defaults to 2.
|
||||
oc_detect_indent() {
|
||||
local file="$1"
|
||||
local indent_str; indent_str="$(awk '/^[[:space:]]+[^[:space:]]/ { match($0, /^[[:space:]]+/); print substr($0, 1, RLENGTH); exit }' "$file")"
|
||||
if [[ -z "$indent_str" ]]; then
|
||||
echo "2"
|
||||
return
|
||||
fi
|
||||
case "$indent_str" in
|
||||
$'\t'*) echo "tab" ;;
|
||||
" "*) echo "4" ;;
|
||||
*) echo "2" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# oc_config_merge <built_file> <existing_file> <output_file>
|
||||
# Merges built opencode.json into an existing one:
|
||||
# - If existing doesn't exist → copy built as-is
|
||||
# - If existing is malformed → overwrite with built (with warning)
|
||||
# - Otherwise → merge: our mcp entries overwrite same-name, user keys preserved
|
||||
oc_config_merge() {
|
||||
local built="$1" existing="$2" output="$3"
|
||||
|
||||
# Fresh install — no existing file
|
||||
if [[ ! -f "$existing" ]]; then
|
||||
cp "$built" "$output"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Validate existing file is JSON
|
||||
if ! jq empty "$existing" 2>/dev/null; then
|
||||
warn "Existing opencode.json is malformed — overwriting with built version"
|
||||
cp "$built" "$output"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Detect indentation from existing file
|
||||
local indent; indent="$(oc_detect_indent "$existing")"
|
||||
local jq_indent
|
||||
case "$indent" in
|
||||
tab) jq_indent="--tab" ;;
|
||||
4) jq_indent="--indent 4" ;;
|
||||
*) jq_indent="--indent 2" ;;
|
||||
esac
|
||||
|
||||
# Read our managed MCP entries
|
||||
local our_mcp; our_mcp="$(jq '.mcp // {}' "$built")"
|
||||
|
||||
# Merge: start with existing, overlay our mcp entries.
|
||||
# Always write to a temp file first to support in-place merges (output == existing).
|
||||
local tmp; tmp="$(mktemp)"
|
||||
# shellcheck disable=SC2086
|
||||
jq $jq_indent --argjson our_mcp "$our_mcp" '
|
||||
.mcp = ((.mcp // {}) + $our_mcp)
|
||||
' "$existing" > "$tmp"
|
||||
mv "$tmp" "$output"
|
||||
}
|
||||
@@ -129,6 +129,12 @@ case "$PLATFORM" in
|
||||
esac
|
||||
PLATFORM_VAULT_DIR="$VAULT_COMPONENTS_DIR"
|
||||
|
||||
# Load opencode-specific helpers when building for opencode
|
||||
if [[ "$PLATFORM" == "opencode" ]]; then
|
||||
# shellcheck source=adapters/opencode/config-merge.sh
|
||||
source "$REPO_DIR/adapters/opencode/config-merge.sh"
|
||||
fi
|
||||
|
||||
# ── Migrate legacy manifests (if any) ────────────────────────────────────────
|
||||
manifest_migrate
|
||||
|
||||
@@ -213,7 +219,12 @@ install_dispatcher "$DISPATCHER_SRC" "$DISPATCHER_DST"
|
||||
|
||||
# ── MCP / opencode.json ───────────────────────────────────────────────────────
|
||||
if [[ -f "$MCP_SRC" ]]; then
|
||||
copy_if_changed "$MCP_SRC" "$MCP_DST"
|
||||
if [[ "$PLATFORM" == "opencode" && -f "$MCP_DST" ]]; then
|
||||
oc_config_merge "$MCP_SRC" "$MCP_DST" "$MCP_DST"
|
||||
info "Merged opencode.json (user config preserved)"
|
||||
else
|
||||
copy_if_changed "$MCP_SRC" "$MCP_DST"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Done ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -106,6 +106,12 @@ case "$PLATFORM" in
|
||||
esac
|
||||
PLATFORM_VAULT_DIR="$VAULT_COMPONENTS_DIR"
|
||||
|
||||
# Load opencode-specific helpers when building for opencode
|
||||
if [[ "$PLATFORM" == "opencode" ]]; then
|
||||
# shellcheck source=adapters/opencode/config-merge.sh
|
||||
source "$REPO_DIR/adapters/opencode/config-merge.sh"
|
||||
fi
|
||||
|
||||
# ── Migrate legacy manifests (if any) ────────────────────────────────────────
|
||||
manifest_migrate
|
||||
|
||||
@@ -142,7 +148,12 @@ CLAUDE_MD_CHANGED=$_LAST_CHANGED
|
||||
|
||||
# ── MCP / opencode.json ───────────────────────────────────────────────────────
|
||||
if [[ -f "$MCP_SRC" ]]; then
|
||||
copy_if_changed "$MCP_SRC" "$MCP_DST"
|
||||
if [[ "$PLATFORM" == "opencode" && -f "$MCP_DST" ]]; then
|
||||
oc_config_merge "$MCP_SRC" "$MCP_DST" "$MCP_DST"
|
||||
info "Merged opencode.json (user config preserved)"
|
||||
else
|
||||
copy_if_changed "$MCP_SRC" "$MCP_DST"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Summary ───────────────────────────────────────────────────────────────────
|
||||
|
||||
172
tests/adapters/opencode/config-merge.test.sh
Executable file
172
tests/adapters/opencode/config-merge.test.sh
Executable file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tests for adapters/opencode/config-merge.sh
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
||||
source "$ROOT/adapters/lib.sh"
|
||||
source "$ROOT/adapters/opencode/config-merge.sh"
|
||||
|
||||
test_oc_merge_fresh_install() {
|
||||
local built; built="$(mktemp)"
|
||||
local existing; existing="$(mktemp)"
|
||||
local output; output="$(mktemp)"
|
||||
rm "$existing"
|
||||
cat > "$built" <<'HEREDOC'
|
||||
{
|
||||
"mcp": {
|
||||
"Gmail": {
|
||||
"type": "remote",
|
||||
"url": "https://gmail.mcp.claude.com/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
HEREDOC
|
||||
oc_config_merge "$built" "$existing" "$output"
|
||||
local result=0
|
||||
[[ -f "$output" ]] || { echo "output not created"; result=1; }
|
||||
jq -e '.mcp.Gmail.url' "$output" >/dev/null || { echo "Gmail not in output"; result=1; }
|
||||
rm -f "$built" "$output"
|
||||
return $result
|
||||
}
|
||||
|
||||
test_oc_merge_preserves_user_keys() {
|
||||
local built; built="$(mktemp)"
|
||||
local existing; existing="$(mktemp)"
|
||||
local output; output="$(mktemp)"
|
||||
cat > "$built" <<'HEREDOC'
|
||||
{
|
||||
"mcp": {
|
||||
"Gmail": {
|
||||
"type": "remote",
|
||||
"url": "https://gmail.mcp.claude.com/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
HEREDOC
|
||||
cat > "$existing" <<'HEREDOC'
|
||||
{
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
"small_model": "anthropic/claude-haiku-4-5",
|
||||
"mcp": {
|
||||
"MyCustomServer": {
|
||||
"type": "local",
|
||||
"command": "my-server"
|
||||
}
|
||||
}
|
||||
}
|
||||
HEREDOC
|
||||
oc_config_merge "$built" "$existing" "$output"
|
||||
local result=0
|
||||
jq -e '.mcp.Gmail.url' "$output" >/dev/null || { echo "Gmail missing"; result=1; }
|
||||
jq -e '.model == "anthropic/claude-sonnet-4-5"' "$output" >/dev/null || { echo "model key lost"; result=1; }
|
||||
jq -e '.small_model == "anthropic/claude-haiku-4-5"' "$output" >/dev/null || { echo "small_model key lost"; result=1; }
|
||||
jq -e '.mcp.MyCustomServer.command == "my-server"' "$output" >/dev/null || { echo "user MCP server lost"; result=1; }
|
||||
rm -f "$built" "$existing" "$output"
|
||||
return $result
|
||||
}
|
||||
|
||||
test_oc_merge_our_mcp_overwrites_same_name() {
|
||||
local built; built="$(mktemp)"
|
||||
local existing; existing="$(mktemp)"
|
||||
local output; output="$(mktemp)"
|
||||
cat > "$built" <<'HEREDOC'
|
||||
{
|
||||
"mcp": {
|
||||
"Gmail": {
|
||||
"type": "remote",
|
||||
"url": "https://gmail.mcp.claude.com/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
HEREDOC
|
||||
cat > "$existing" <<'HEREDOC'
|
||||
{
|
||||
"mcp": {
|
||||
"Gmail": {
|
||||
"type": "remote",
|
||||
"url": "https://old-url.example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
HEREDOC
|
||||
oc_config_merge "$built" "$existing" "$output"
|
||||
local result=0
|
||||
local url; url="$(jq -r '.mcp.Gmail.url' "$output")"
|
||||
[[ "$url" == "https://gmail.mcp.claude.com/mcp" ]] || { echo "our url should win: got $url"; result=1; }
|
||||
rm -f "$built" "$existing" "$output"
|
||||
return $result
|
||||
}
|
||||
|
||||
test_oc_merge_detects_indentation() {
|
||||
local built; built="$(mktemp)"
|
||||
local existing; existing="$(mktemp)"
|
||||
local output; output="$(mktemp)"
|
||||
cat > "$built" <<'HEREDOC'
|
||||
{
|
||||
"mcp": {
|
||||
"Gmail": {
|
||||
"type": "remote",
|
||||
"url": "https://gmail.mcp.claude.com/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
HEREDOC
|
||||
cat > "$existing" <<'HEREDOC'
|
||||
{
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
"mcp": {}
|
||||
}
|
||||
HEREDOC
|
||||
oc_config_merge "$built" "$existing" "$output"
|
||||
local result=0
|
||||
grep -q '^ "model"' "$output" || { echo "expected 4-space indent"; cat "$output"; result=1; }
|
||||
rm -f "$built" "$existing" "$output"
|
||||
return $result
|
||||
}
|
||||
|
||||
test_oc_merge_malformed_existing_falls_back() {
|
||||
local built; built="$(mktemp)"
|
||||
local existing; existing="$(mktemp)"
|
||||
local output; output="$(mktemp)"
|
||||
cat > "$built" <<'HEREDOC'
|
||||
{
|
||||
"mcp": {
|
||||
"Gmail": {
|
||||
"type": "remote",
|
||||
"url": "https://gmail.mcp.claude.com/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
HEREDOC
|
||||
echo "this is not json" > "$existing"
|
||||
oc_config_merge "$built" "$existing" "$output" 2>/dev/null
|
||||
local result=0
|
||||
jq -e '.mcp.Gmail.url' "$output" >/dev/null || { echo "fallback failed"; result=1; }
|
||||
rm -f "$built" "$existing" "$output"
|
||||
return $result
|
||||
}
|
||||
|
||||
test_oc_merge_no_mcp_in_existing() {
|
||||
local built; built="$(mktemp)"
|
||||
local existing; existing="$(mktemp)"
|
||||
local output; output="$(mktemp)"
|
||||
cat > "$built" <<'HEREDOC'
|
||||
{
|
||||
"mcp": {
|
||||
"Gmail": {
|
||||
"type": "remote",
|
||||
"url": "https://gmail.mcp.claude.com/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
HEREDOC
|
||||
cat > "$existing" <<'HEREDOC'
|
||||
{
|
||||
"model": "anthropic/claude-sonnet-4-5"
|
||||
}
|
||||
HEREDOC
|
||||
oc_config_merge "$built" "$existing" "$output"
|
||||
local result=0
|
||||
jq -e '.model == "anthropic/claude-sonnet-4-5"' "$output" >/dev/null || { echo "model key lost"; result=1; }
|
||||
jq -e '.mcp.Gmail.url' "$output" >/dev/null || { echo "Gmail not added"; result=1; }
|
||||
rm -f "$built" "$existing" "$output"
|
||||
return $result
|
||||
}
|
||||
Reference in New Issue
Block a user