From 23d7780b77ec375beb862fa2202c0626bd9f40ad Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sun, 3 May 2026 12:50:18 -0400 Subject: [PATCH] fix(opencode): refresh stale kimaki wrapper --- .github/workflows/shell.yml | 8 +++ runtimes/opencode.sh | 67 ++++++++++++++----- tests/opencode-wrapper.sh | 125 ++++++++++++++++++++++++++++++++++++ upgrade.sh | 22 ++++++- 4 files changed, 203 insertions(+), 19 deletions(-) create mode 100755 tests/opencode-wrapper.sh diff --git a/.github/workflows/shell.yml b/.github/workflows/shell.yml index 3d786fd..f780da5 100644 --- a/.github/workflows/shell.yml +++ b/.github/workflows/shell.yml @@ -31,3 +31,11 @@ jobs: - uses: actions/checkout@v4 - name: Run tests/bridge-render.sh run: ./tests/bridge-render.sh + + opencode-wrapper: + name: opencode wrapper regression + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run tests/opencode-wrapper.sh + run: ./tests/opencode-wrapper.sh diff --git a/runtimes/opencode.sh b/runtimes/opencode.sh index 8d1cb2e..c57d31a 100644 --- a/runtimes/opencode.sh +++ b/runtimes/opencode.sh @@ -33,18 +33,31 @@ _install_opencode_wrapper() { local OPENCODE_BIN OPENCODE_BIN=$(command -v opencode 2>/dev/null || echo "/usr/bin/opencode") - # Don't wrap if opencode is already a wrapper script (re-runs) - if head -1 "$OPENCODE_BIN" 2>/dev/null | grep -q "bash"; then - log "OpenCode wrapper already installed — skipping" - return - fi - # Find the real opencode binary (after npm install, it's in node_modules) local REAL_BIN REAL_BIN=$(readlink -f "$OPENCODE_BIN" 2>/dev/null || echo "$OPENCODE_BIN") - # If the resolved path is still a wrapper, dig deeper + # If the resolved path is still a wrapper, dig deeper. Existing installs may + # have old wp-coding-agents wrappers at /usr/local/bin/opencode; parse their + # exec target before falling back to the npm global package path. if head -1 "$REAL_BIN" 2>/dev/null | grep -q "bash"; then - REAL_BIN="/usr/lib/node_modules/opencode-ai/bin/opencode" + local WRAPPED_REAL_BIN + WRAPPED_REAL_BIN="" + while IFS= read -r line; do + case "$line" in + exec\ *opencode*|exec\ /*opencode*) + set -- $line + WRAPPED_REAL_BIN="${2#\'}" + WRAPPED_REAL_BIN="${WRAPPED_REAL_BIN%\'}" + WRAPPED_REAL_BIN="${WRAPPED_REAL_BIN#\"}" + WRAPPED_REAL_BIN="${WRAPPED_REAL_BIN%\"}" + ;; + esac + done < "$REAL_BIN" + if [ -n "$WRAPPED_REAL_BIN" ] && [ -f "$WRAPPED_REAL_BIN" ]; then + REAL_BIN="$WRAPPED_REAL_BIN" + else + REAL_BIN="/usr/lib/node_modules/opencode-ai/bin/opencode" + fi fi if [ ! -f "$REAL_BIN" ]; then @@ -52,12 +65,13 @@ _install_opencode_wrapper() { return fi - log "Installing OpenCode credential sync wrapper..." - - local WRAPPER_CONTENT='#!/usr/bin/env bash + local WRAPPER_CONTENT + WRAPPER_CONTENT=$(cat <<'EOF' +#!/usr/bin/env bash +# wp-coding-agents-opencode-wrapper-v2 set -euo pipefail -# Syncs Anthropic credentials from Kimaki'\''s account store into the format +# Syncs Anthropic credentials from Kimaki's account store into the format # opencode-claude-auth reads (~/.claude/.credentials.json). Kimaki manages # OAuth token refresh — this wrapper forwards fresh tokens on each invocation. @@ -65,7 +79,7 @@ KIMAKI_ACCOUNTS="${HOME}/.local/share/opencode/anthropic-oauth-accounts.json" CLAUDE_CREDENTIALS="${HOME}/.claude/.credentials.json" if [[ -f "$KIMAKI_ACCOUNTS" ]] && command -v node >/dev/null 2>&1; then - node -e '"'"' + node -e ' const fs = require("fs"); const path = require("path"); try { @@ -91,7 +105,7 @@ if [[ -f "$KIMAKI_ACCOUNTS" ]] && command -v node >/dev/null 2>&1; then fs.writeFileSync(claudePath, JSON.stringify(creds, null, 2), { mode: 0o600 }); } } catch {} - '"'"' 2>/dev/null || true + ' 2>/dev/null || true fi # Legacy sync: claude creds → opencode auth.json (fallback for built-in auth) @@ -101,15 +115,34 @@ if [[ -f "$CLAUDE_CREDENTIALS" ]] && command -v jq >/dev/null 2>&1; then jq "{anthropic:{type:\"oauth\",refresh:(.claudeAiOauth.refreshToken//error(\"missing\")),access:(.claudeAiOauth.accessToken//error(\"missing\")),expires:(.claudeAiOauth.expiresAt//error(\"missing\"))}}" "$CLAUDE_CREDENTIALS" > "${AUTH_DST}.tmp" 2>/dev/null && mv "${AUTH_DST}.tmp" "$AUTH_DST" fi -exec '"${REAL_BIN}"' "$@" -' +exec "__REAL_BIN__" "$@" +EOF +) + WRAPPER_CONTENT=${WRAPPER_CONTENT/__REAL_BIN__/$REAL_BIN} + + if [ -f "$OPENCODE_BIN" ] && [ "$(cat "$OPENCODE_BIN" 2>/dev/null)" = "$WRAPPER_CONTENT" ]; then + log "OpenCode wrapper already at current version — skipping" + return + fi if [ "$DRY_RUN" = true ]; then - echo -e "${BLUE}[dry-run]${NC} Would install OpenCode wrapper at $OPENCODE_BIN" + if head -1 "$OPENCODE_BIN" 2>/dev/null | grep -q "bash"; then + echo -e "${BLUE}[dry-run]${NC} Would replace OpenCode wrapper at $OPENCODE_BIN" + else + echo -e "${BLUE}[dry-run]${NC} Would install OpenCode wrapper at $OPENCODE_BIN" + fi else + if head -1 "$OPENCODE_BIN" 2>/dev/null | grep -q "bash"; then + local BACKUP_PATH="${OPENCODE_BIN}.bak.$(date +%Y%m%d%H%M%S)" + cp "$OPENCODE_BIN" "$BACKUP_PATH" + log "Replacing outdated OpenCode wrapper at $OPENCODE_BIN (backup: $BACKUP_PATH)" + else + log "Installing OpenCode credential sync wrapper..." + fi echo "$WRAPPER_CONTENT" > "$OPENCODE_BIN" chmod +x "$OPENCODE_BIN" log "Installed credential sync wrapper at $OPENCODE_BIN → $REAL_BIN" + UPDATED_ITEMS+=("opencode credential wrapper") fi } diff --git a/tests/opencode-wrapper.sh b/tests/opencode-wrapper.sh new file mode 100755 index 0000000..5a050b5 --- /dev/null +++ b/tests/opencode-wrapper.sh @@ -0,0 +1,125 @@ +#!/bin/bash +# tests/opencode-wrapper.sh — regression tests for the Kimaki OpenCode wrapper. +set -eu + +SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +cd "$SCRIPT_DIR" + +PASS=0 +FAIL=0 + +log() { :; } +warn() { printf 'WARN: %s\n' "$*" >&2; } + +assert_file_contains() { + local label="$1" file="$2" needle="$3" + if grep -qF "$needle" "$file"; then + echo " ok $label" + PASS=$((PASS+1)) + else + echo " FAIL $label" + echo " expected $file to contain: $needle" + FAIL=$((FAIL+1)) + fi +} + +assert_eq() { + local label="$1" expected="$2" actual="$3" + if [ "$expected" = "$actual" ]; then + echo " ok $label" + PASS=$((PASS+1)) + else + echo " FAIL $label" + echo " expected: '$expected'" + echo " actual: '$actual'" + FAIL=$((FAIL+1)) + fi +} + +TMPDIR_TEST="$(mktemp -d)" +trap 'rm -rf "$TMPDIR_TEST"' EXIT + +# shellcheck disable=SC1091 +source runtimes/opencode.sh +UPDATED_ITEMS=() + +make_real_opencode() { + local real_dir="$TMPDIR_TEST/real/bin" + mkdir -p "$real_dir" + cat > "$real_dir/opencode" <<'EOF' +#!/bin/sh +echo real opencode "$@" +EOF + chmod +x "$real_dir/opencode" + printf '%s/opencode' "$real_dir" +} + +make_stale_wrapper() { + local wrapper="$1" real_bin="$2" + cat > "$wrapper" < stale wrapper replacement" +REAL_BIN="$(make_real_opencode)" +BIN_DIR="$TMPDIR_TEST/bin" +mkdir -p "$BIN_DIR" +STALE_WRAPPER="$BIN_DIR/opencode" +make_stale_wrapper "$STALE_WRAPPER" "$REAL_BIN" + +CHAT_BRIDGE=kimaki +LOCAL_MODE=false +DRY_RUN=false +PATH="$BIN_DIR:$PATH" + +_install_opencode_wrapper + +assert_file_contains "writes current wrapper sentinel" "$STALE_WRAPPER" "# wp-coding-agents-opencode-wrapper-v2" +assert_file_contains "preserves real binary target" "$STALE_WRAPPER" "exec \"$REAL_BIN\"" +assert_eq "backs up stale wrapper" "1" "$(ls "$BIN_DIR"/opencode.bak.* 2>/dev/null | wc -l | tr -d ' ')" + +_install_opencode_wrapper + +assert_eq "current wrapper rerun is idempotent" "1" "$(ls "$BIN_DIR"/opencode.bak.* 2>/dev/null | wc -l | tr -d ' ')" + +echo "==> skip gates" +NON_KIMAKI_DIR="$TMPDIR_TEST/non-kimaki/bin" +mkdir -p "$NON_KIMAKI_DIR" +NON_KIMAKI_WRAPPER="$NON_KIMAKI_DIR/opencode" +make_stale_wrapper "$NON_KIMAKI_WRAPPER" "$REAL_BIN" +CHAT_BRIDGE=telegram +LOCAL_MODE=false +PATH="$NON_KIMAKI_DIR:$PATH" +_install_opencode_wrapper +assert_eq "non-kimaki bridge leaves wrapper alone" "0" "$(grep -c 'wp-coding-agents-opencode-wrapper-v2' "$NON_KIMAKI_WRAPPER")" + +LOCAL_DIR="$TMPDIR_TEST/local/bin" +mkdir -p "$LOCAL_DIR" +LOCAL_WRAPPER="$LOCAL_DIR/opencode" +make_stale_wrapper "$LOCAL_WRAPPER" "$REAL_BIN" +CHAT_BRIDGE=kimaki +LOCAL_MODE=true +PATH="$LOCAL_DIR:$PATH" +_install_opencode_wrapper +assert_eq "local mode leaves wrapper alone" "0" "$(grep -c 'wp-coding-agents-opencode-wrapper-v2' "$LOCAL_WRAPPER")" + +echo "==> upgrade phase guard" +assert_file_contains "upgrade sources opencode runtime for kimaki" upgrade.sh 'source "$SCRIPT_DIR/runtimes/opencode.sh"' +assert_file_contains "upgrade invokes wrapper refresh" upgrade.sh "_install_opencode_wrapper" + +echo +if [ "$FAIL" -gt 0 ]; then + echo "FAILED: $FAIL of $((PASS+FAIL)) assertion(s)" + exit 1 +fi +echo "OK: $PASS / $PASS assertions passed" diff --git a/upgrade.sh b/upgrade.sh index 621eb70..21d1b6e 100755 --- a/upgrade.sh +++ b/upgrade.sh @@ -626,12 +626,30 @@ update_chat_bridge_launchd() { reapply_claude_auth_patch() { _run_filter_active patch || return 0 + if [ "$RUNTIME" != "opencode" ] && [ "$CHAT_BRIDGE" != "kimaki" ]; then + log "Phase 7: Skipping (runtime is $RUNTIME and chat bridge is $CHAT_BRIDGE)" + return 0 + fi + + log "Phase 7: Checking OpenCode auth integration..." + + if ! declare -F _install_opencode_wrapper >/dev/null; then + # Kimaki spawns opencode-serve even when the primary editing runtime is not + # OpenCode, so stale wrappers must be repairable from any Kimaki install. + # Source the runtime file for its wrapper helper without running a full + # OpenCode runtime install. + # shellcheck disable=SC1091 + source "$SCRIPT_DIR/runtimes/opencode.sh" + fi + + _install_opencode_wrapper + if [ "$RUNTIME" != "opencode" ]; then - log "Phase 7: Skipping (runtime is $RUNTIME, not opencode)" + log " Skipping opencode-claude-auth patch (runtime is $RUNTIME)" return 0 fi - log "Phase 7: Re-applying opencode-claude-auth PascalCase patch..." + log " Re-applying opencode-claude-auth PascalCase patch..." if [ ! -f "$SCRIPT_DIR/lib/patch-claude-auth.py" ]; then warn " patch-claude-auth.py not found — skipping"