From 042316ea6a57f097c7d07be66beba87cf819f401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Mon, 11 May 2026 00:52:54 +0300 Subject: [PATCH] =?UTF-8?q?feat(scripts):=20setup-secrets.sh=20=E2=80=94?= =?UTF-8?q?=20placeholder=20resolution=20with=20--secret/--skip/--list=20m?= =?UTF-8?q?odes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/lib/setup-secrets.sh | 85 +++++++++++++++++++++++++++++ scripts/tests/setup-secrets-test.sh | 55 +++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 scripts/lib/setup-secrets.sh create mode 100644 scripts/tests/setup-secrets-test.sh diff --git a/scripts/lib/setup-secrets.sh b/scripts/lib/setup-secrets.sh new file mode 100644 index 0000000..256d9cc --- /dev/null +++ b/scripts/lib/setup-secrets.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# Resolve secret placeholders <> in target file +# Modes: +# --secret=NAME=VALUE (non-interactive substitution; can be repeated) +# --list-unresolved (find <<*>> placeholders, no changes) +# --skip-unresolved (don't prompt for unresolved; leave + write to .brain-deferred-secrets.txt) +# (default) interactive prompt for each placeholder not provided via --secret +# +# Usage: setup-secrets.sh [--secret=NAME=VALUE ...] [--skip-unresolved | --list-unresolved] +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +declare -A secrets=() +skip_unresolved=0 +list_only=0 +target="" + +while [ $# -gt 0 ]; do + case "$1" in + --secret=*) + kv="${1#--secret=}" + name="${kv%%=*}" + value="${kv#*=}" + secrets["$name"]="$value" + ;; + --skip-unresolved) skip_unresolved=1 ;; + --list-unresolved) list_only=1 ;; + *) target="$1" ;; + esac + shift +done + +[ -n "$target" ] || { log_error "Target file required"; exit 1; } +[ -f "$target" ] || { log_error "Target not found: $target"; exit 1; } + +# Find all placeholders (e.g. <>); uppercase-only names. +placeholders=$(grep -oE '<<[A-Z_][A-Z0-9_]*>>' "$target" 2>/dev/null | sort -u || true) + +if [ "$list_only" -eq 1 ]; then + if [ -z "$placeholders" ]; then + log_info "No unresolved placeholders in $target" + else + log_info "Unresolved placeholders in $target:" + echo "$placeholders" + fi + exit 0 +fi + +deferred_file="$(dirname "$target")/.brain-deferred-secrets.txt" +deferred="" + +for p in $placeholders; do + name="${p#<<}" + name="${name%>>}" + + if [ -n "${secrets[$name]:-}" ]; then + value="${secrets[$name]}" + # sed-replace via temp + mv (Windows-safe; avoids sed -i portability issues) + sed "s|<<$name>>|$value|g" "$target" > "$target.tmp" + mv "$target.tmp" "$target" + log_info "Resolved <<$name>>" + elif [ "$skip_unresolved" -eq 1 ]; then + deferred="$deferred$name\n" + log_warn "Skipped <<$name>> (deferred)" + else + # Interactive prompt (manual usage; tests always pass --secret or --skip) + printf "Enter value for <<%s>> (or empty to skip): " "$name" >&2 + read -r value + if [ -n "$value" ]; then + sed "s|<<$name>>|$value|g" "$target" > "$target.tmp" + mv "$target.tmp" "$target" + log_info "Resolved <<$name>>" + else + deferred="$deferred$name\n" + log_warn "Skipped <<$name>>" + fi + fi +done + +if [ -n "$deferred" ]; then + printf "%b" "$deferred" > "$deferred_file" + log_warn "Deferred secrets recorded: $deferred_file" +fi diff --git a/scripts/tests/setup-secrets-test.sh b/scripts/tests/setup-secrets-test.sh new file mode 100644 index 0000000..4cc50a0 --- /dev/null +++ b/scripts/tests/setup-secrets-test.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +set -u +SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +source "$SCRIPT_DIR/lib/common.sh" + +FAILURES=0 +assert_eq() { + if [ "$1" = "$2" ]; then echo "PASS: $3"; else echo "FAIL: $3 (expected '$1', got '$2')"; FAILURES=$((FAILURES + 1)); fi +} + +# Test 1: --secret=KEY=VALUE replaces placeholder +tmpdir=$(mktemp -d) +cat > "$tmpdir/config.json" <>"] + } + } +} +EOF2 + +bash "$SCRIPT_DIR/lib/setup-secrets.sh" --secret=MAGIC_API_KEY=resolved_value "$tmpdir/config.json" + +result=$(jq -r '.mcpServers.magic.args[0]' "$tmpdir/config.json") +assert_eq "API_KEY=resolved_value" "$result" "non-interactive: placeholder replaced" + +# Test 2: --skip-unresolved leaves placeholder + creates deferred file +cat > "$tmpdir/config2.json" <>", "TOKEN=<>"] +} +EOF3 +bash "$SCRIPT_DIR/lib/setup-secrets.sh" --skip-unresolved --secret=MAGIC_API_KEY=val "$tmpdir/config2.json" +remaining=$(jq -r '.args[1]' "$tmpdir/config2.json") +assert_eq "TOKEN=<>" "$remaining" "skip-unresolved: leaves unknown placeholder" + +# Test 3: list-unresolved shows placeholders without replacing +cat > "$tmpdir/config3.json" <>", "k2": "<>", "k3": "<>"} +EOF4 +output=$(bash "$SCRIPT_DIR/lib/setup-secrets.sh" --list-unresolved "$tmpdir/config3.json" 2>&1) +case "$output" in + *"<>"*"<>"*) assert_eq "ok" "ok" "list-unresolved finds both" ;; + *) assert_eq "ok" "fail" "list-unresolved finds both" ;; +esac +# Verify file untouched +unchanged=$(jq -r '.k1' "$tmpdir/config3.json") +assert_eq "<>" "$unchanged" "list-unresolved: file untouched" + +rm -rf "$tmpdir" + +echo "---" +echo "Failures: $FAILURES" +exit $FAILURES