#!/usr/bin/env bash # scripts/setenv.sh — set or append KEY=VALUE lines in .env, safely. # # Why this exists: the operator administers this box from an iPhone, through # the AWS console's browser terminal. In that environment `nano` is close to # unusable and a pasted multi-line block gets swallowed by whatever the first # line started. Every config change therefore has to be ONE line, with no # interactive editor and no heredoc. # # ./scripts/setenv.sh SOLANA_LEG2_HAIRCUT_BPS=2 MIN_PROFIT_FLOOR_USD=0.20 # # Behaviour: # • replaces the line if the key already exists (commented or not) # • appends it if it doesn't # • takes ONE timestamped backup per run, so a bad paste is recoverable # • redacts by KEY NAME — anything matching KEY/TOKEN/SECRET/PASSWORD/ # PRIVATE/RPC/URL prints as `[REDACTED — n chars set]`, with no # recoverable characters, so the output is safe to screenshot # # Values are written verbatim, quoted or not, so URLs with `/` and `?` are # safe — this uses awk on exact key matches rather than a sed substitution # whose delimiter the value could contain. That is the specific bug that # makes `sed -i "s|^KEY=.*|KEY=$URL|"` a bad idea for RPC endpoints. set -euo pipefail cd "$(dirname "$0")/.." ENV_FILE="${ENV_FILE:-.env}" if [ "$#" -eq 0 ]; then echo "usage: ./scripts/setenv.sh KEY=VALUE [KEY=VALUE ...]" >&2 exit 64 fi [ -f "$ENV_FILE" ] || { echo "no $ENV_FILE here — are you in ~/Garden-Angel-Terminal?" >&2; exit 66; } BACKUP="${ENV_FILE}.bak.$(date +%Y%m%d-%H%M%S)" cp "$ENV_FILE" "$BACKUP" # Decide from the KEY NAME, not the value's length. # # The first version of this inspected only the value: a `case` branch that # did nothing, then "print in full if 24 chars or shorter". That got both # halves wrong. A short API key printed verbatim, and even a long one leaked # a 6-character prefix and 4-character suffix — on a script whose entire # stated purpose is being safe to screenshot. # # Key names are the reliable signal. A value cannot tell you whether it is a # secret; `PUMPFUN_API_KEY` can. Secrets now print a FIXED placeholder with # no recoverable characters and no length hint, regardless of how long they # are. Non-secret settings (thresholds, booleans, sizes) print in full, # because seeing those confirmed is the point of the output. is_secret_key() { case "$(printf '%s' "$1" | tr '[:lower:]' '[:upper:]')" in *KEY*|*TOKEN*|*SECRET*|*PASSWORD*|*PASSPHRASE*|*PRIVATE*|*RPC*|*URL*|*DSN*|*WEBHOOK*) return 0 ;; esac return 1 } mask() { local k="$1" v="$2" if is_secret_key "$k"; then printf '[REDACTED — %d chars set]' "${#v}" else printf '%s' "$v" fi } for pair in "$@"; do case "$pair" in *=*) : ;; *) echo "skipping '$pair' — expected KEY=VALUE" >&2; continue ;; esac key="${pair%%=*}" val="${pair#*=}" before="$(grep -c "^[#[:space:]]*${key}=" "$ENV_FILE" || true)" # awk, not sed: the value may contain any delimiter character (RPC URLs # contain `/` and sometimes `?`), and awk never re-interprets it. KEY="$key" VAL="$val" awk ' BEGIN { k = ENVIRON["KEY"]; v = ENVIRON["VAL"]; done = 0 } { line = $0 stripped = line sub(/^[#[:space:]]*/, "", stripped) if (!done && index(stripped, k "=") == 1) { print k "=" v; done = 1; next } print line } END { if (!done) print k "=" v } ' "$ENV_FILE" > "${ENV_FILE}.tmp" mv "${ENV_FILE}.tmp" "$ENV_FILE" if [ "$before" -gt 0 ]; then printf ' updated %-34s = %s\n' "$key" "$(mask "$key" "$val")" else printf ' ADDED %-34s = %s\n' "$key" "$(mask "$key" "$val")" fi done chmod 600 "$ENV_FILE" echo echo "backup: $BACKUP" echo "restore with: cp $BACKUP $ENV_FILE"