feat(scripts): setup-secrets.sh — placeholder resolution with --secret/--skip/--list modes

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-05-11 00:52:54 +03:00
parent c37fd3c9e2
commit 042316ea6a
2 changed files with 140 additions and 0 deletions
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# Resolve secret placeholders <<NAME>> 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] <target-file>
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. <<MAGIC_API_KEY>>); 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
+55
View File
@@ -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
{
"mcpServers": {
"magic": {
"args": ["API_KEY=<<MAGIC_API_KEY>>"]
}
}
}
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" <<EOF3
{
"args": ["API_KEY=<<MAGIC_API_KEY>>", "TOKEN=<<UNKNOWN_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=<<UNKNOWN_TOKEN>>" "$remaining" "skip-unresolved: leaves unknown placeholder"
# Test 3: list-unresolved shows placeholders without replacing
cat > "$tmpdir/config3.json" <<EOF4
{"k1": "<<A>>", "k2": "<<B>>", "k3": "<<A>>"}
EOF4
output=$(bash "$SCRIPT_DIR/lib/setup-secrets.sh" --list-unresolved "$tmpdir/config3.json" 2>&1)
case "$output" in
*"<<A>>"*"<<B>>"*) 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 "<<A>>" "$unchanged" "list-unresolved: file untouched"
rm -rf "$tmpdir"
echo "---"
echo "Failures: $FAILURES"
exit $FAILURES