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:
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user