| 1 | #!/bin/bash |
| 2 | # Store last30days API keys in the macOS Keychain. |
| 3 | # |
| 4 | # Keys are stored as generic passwords with service name `last30days-<KEY>` |
| 5 | # for the current user. The lib/env.py loader picks them up automatically as |
| 6 | # the lowest-priority credential source on Darwin. |
| 7 | # |
| 8 | # Usage: |
| 9 | # ./setup-keychain.sh # interactive: prompts for each key |
| 10 | # ./setup-keychain.sh KEY [KEY..] # prompt only for the listed keys |
| 11 | # ./setup-keychain.sh --list # list which last30days-* items exist |
| 12 | # ./setup-keychain.sh --delete KEY # remove a stored key |
| 13 | # |
| 14 | # Existing values are shown as "(set)" and skipped unless --replace is passed. |
| 15 | # Skip any prompt with empty input. |
| 16 | |
| 17 | set -euo pipefail |
| 18 | |
| 19 | PREFIX="last30days-" |
| 20 | # Mirrors lib/env.py::KEYCHAIN_KEYS — kept in sync via |
| 21 | # tests/test_env_keychain.py::test_keychain_keys_match_setup_script. |
| 22 | ALL_KEYS=( |
| 23 | OPENAI_API_KEY |
| 24 | XAI_API_KEY |
| 25 | GOOGLE_API_KEY |
| 26 | GEMINI_API_KEY |
| 27 | GOOGLE_GENAI_API_KEY |
| 28 | SCRAPECREATORS_API_KEY |
| 29 | APIFY_API_TOKEN |
| 30 | AUTH_TOKEN |
| 31 | CT0 |
| 32 | BSKY_HANDLE |
| 33 | BSKY_APP_PASSWORD |
| 34 | TRUTHSOCIAL_TOKEN |
| 35 | BRAVE_API_KEY |
| 36 | EXA_API_KEY |
| 37 | SERPER_API_KEY |
| 38 | OPENROUTER_API_KEY |
| 39 | PERPLEXITY_API_KEY |
| 40 | PARALLEL_API_KEY |
| 41 | XQUIK_API_KEY |
| 42 | XIAOHONGSHU_API_BASE |
| 43 | GITHUB_TOKEN |
| 44 | ) |
| 45 | |
| 46 | if [[ "${OSTYPE:-}" != darwin* ]]; then |
| 47 | echo "setup-keychain.sh requires macOS (security command). Got: $OSTYPE" >&2 |
| 48 | exit 1 |
| 49 | fi |
| 50 | if ! command -v security >/dev/null 2>&1; then |
| 51 | echo "security command not found on PATH" >&2 |
| 52 | exit 1 |
| 53 | fi |
| 54 | |
| 55 | REPLACE=0 |
| 56 | ACTION="prompt" |
| 57 | TARGETS=() |
| 58 | |
| 59 | while [[ $# -gt 0 ]]; do |
| 60 | case "$1" in |
| 61 | --list) ACTION="list"; shift ;; |
| 62 | --delete) ACTION="delete"; shift ;; |
| 63 | --replace) REPLACE=1; shift ;; |
| 64 | --help|-h) sed -n '2,/^$/p' "$0" | sed 's/^# //; s/^#//'; exit 0 ;; |
| 65 | -*) echo "unknown flag: $1" >&2; exit 2 ;; |
| 66 | *) TARGETS+=("$1"); shift ;; |
| 67 | esac |
| 68 | done |
| 69 | |
| 70 | case "$ACTION" in |
| 71 | list) |
| 72 | echo "Stored last30days-* keychain items:" |
| 73 | for key in "${ALL_KEYS[@]}"; do |
| 74 | if security find-generic-password -a "$USER" -s "${PREFIX}${key}" -w >/dev/null 2>&1; then |
| 75 | echo " $key" |
| 76 | fi |
| 77 | done |
| 78 | exit 0 |
| 79 | ;; |
| 80 | delete) |
| 81 | if [[ ${#TARGETS[@]} -eq 0 ]]; then |
| 82 | echo "--delete needs at least one KEY name" >&2; exit 2 |
| 83 | fi |
| 84 | for key in "${TARGETS[@]}"; do |
| 85 | if security delete-generic-password -a "$USER" -s "${PREFIX}${key}" >/dev/null 2>&1; then |
| 86 | echo "deleted: $key" |
| 87 | else |
| 88 | echo "not found: $key" |
| 89 | fi |
| 90 | done |
| 91 | exit 0 |
| 92 | ;; |
| 93 | esac |
| 94 | |
| 95 | if [[ ${#TARGETS[@]} -eq 0 ]]; then |
| 96 | TARGETS=("${ALL_KEYS[@]}") |
| 97 | fi |
| 98 | |
| 99 | added=0; skipped=0; replaced=0 |
| 100 | for key in "${TARGETS[@]}"; do |
| 101 | existing="$(security find-generic-password -a "$USER" -s "${PREFIX}${key}" -w 2>/dev/null || true)" |
| 102 | if [[ -n "$existing" && "$REPLACE" -eq 0 ]]; then |
| 103 | printf " %-28s (set, skipping — use --replace to overwrite)\n" "$key" |
| 104 | skipped=$((skipped + 1)) |
| 105 | continue |
| 106 | fi |
| 107 | printf " %-28s " "$key" |
| 108 | IFS= read -rs value |
| 109 | echo |
| 110 | if [[ -z "$value" ]]; then |
| 111 | skipped=$((skipped + 1)) |
| 112 | continue |
| 113 | fi |
| 114 | security add-generic-password -U -a "$USER" -s "${PREFIX}${key}" -w "$value" |
| 115 | if [[ -n "$existing" ]]; then |
| 116 | replaced=$((replaced + 1)) |
| 117 | else |
| 118 | added=$((added + 1)) |
| 119 | fi |
| 120 | done |
| 121 | |
| 122 | echo |
| 123 | echo "Done. added=$added replaced=$replaced skipped=$skipped" |
| 124 | echo "Verify with: $0 --list" |
| 125 |