|
#!/usr/bin/env bash |
|
# Interactive bootstrap for gclaude (Claude Code + Vertex AI). |
|
# Run: bash setup-gclaude.sh |
|
# Remote: curl -fsSL 'RAW_URL' | bash |
|
# Prompts use /dev/tty so keyboard input works even when stdin is the script pipe. |
|
# See GIST.md for gist.github.com. |
|
# Docs: https://code.claude.com/docs/en/google-vertex-ai |
|
set -euo pipefail |
|
|
|
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/gclaude" |
|
LOCAL_BIN="${HOME}/.local/bin" |
|
KEY_FILE="${CONFIG_DIR}/gcp-key.json" |
|
ENV_FILE="${CONFIG_DIR}/env" |
|
GCLAUDE_BIN="${LOCAL_BIN}/gclaude" |
|
|
|
die() { |
|
echo "setup-gclaude: $*" >&2 |
|
exit 1 |
|
} |
|
|
|
# Read from /dev/tty so prompts work when stdin is a pipe (e.g. curl | bash). |
|
read_tty() { |
|
read -r "$@" </dev/tty |
|
} |
|
|
|
validate_json_file() { |
|
python3 -c 'import json, sys; json.load(open(sys.argv[1], encoding="utf-8"))' "$1" || |
|
die "invalid JSON: $1" |
|
} |
|
|
|
prompt_yes() { |
|
local reply |
|
read_tty -p "$1 [y/N] " reply |
|
case "${reply}" in |
|
[yY]|[yY][eE][sS]) return 0 ;; |
|
*) return 1 ;; |
|
esac |
|
} |
|
|
|
echo "=== gclaude bootstrap ===" |
|
echo |
|
|
|
read_tty -p "GCP project ID (ANTHROPIC_VERTEX_PROJECT_ID): " PROJECT_ID |
|
[[ -n "${PROJECT_ID}" ]] || die "project ID is required" |
|
|
|
read_tty -p "Vertex region [global]: " REGION |
|
REGION="${REGION:-global}" |
|
|
|
echo |
|
echo "Service account JSON key — choose how to provide it:" |
|
echo " 1) Path to a file on disk (recommended)" |
|
echo " 2) Paste in this terminal (full JSON; end with Ctrl-D, often twice if the cursor is mid-line)" |
|
if command -v pbpaste >/dev/null 2>&1; then |
|
echo " 3) Use macOS clipboard (copy the JSON in another app first, then choose 3)" |
|
fi |
|
read_tty -p "Choice [1]: " KEY_MODE |
|
KEY_MODE="${KEY_MODE:-1}" |
|
|
|
mkdir -p "${CONFIG_DIR}" |
|
|
|
case "${KEY_MODE}" in |
|
1) |
|
read_tty -p "Absolute path to JSON key file: " KEY_PATH |
|
[[ -n "${KEY_PATH}" ]] || die "path is required for option 1" |
|
[[ -f "${KEY_PATH}" ]] || die "file not found: ${KEY_PATH}" |
|
cp "${KEY_PATH}" "${KEY_FILE}" |
|
;; |
|
2) |
|
echo |
|
echo "Paste the entire JSON key below, then press Ctrl-D to finish." |
|
# Read from /dev/tty so a multi-line paste is never split across bash \`read\` + \`cat\`, and Ctrl-D ends input on the terminal. |
|
python3 - "${KEY_FILE}" <<'PY' |
|
import sys |
|
|
|
path = sys.argv[1] |
|
try: |
|
with open("/dev/tty", encoding="utf-8", errors="replace") as tty: |
|
data = tty.read() |
|
except OSError: |
|
# No controlling tty (e.g. CI); read pasted input from stdin instead. |
|
data = sys.stdin.read() |
|
if not data.strip(): |
|
print("setup-gclaude: no key content received (empty after Ctrl-D)", file=sys.stderr) |
|
sys.exit(1) |
|
open(path, "w", encoding="utf-8").write(data) |
|
PY |
|
;; |
|
3) |
|
command -v pbpaste >/dev/null 2>&1 || die "pbpaste not available; use option 1 or 2" |
|
pbpaste >"${KEY_FILE}" || die "pbpaste failed" |
|
;; |
|
*) |
|
die "invalid choice: ${KEY_MODE}" |
|
;; |
|
esac |
|
|
|
validate_json_file "${KEY_FILE}" |
|
chmod 600 "${KEY_FILE}" |
|
|
|
if [[ -f "${ENV_FILE}" ]] && ! prompt_yes "Overwrite existing ${ENV_FILE}?"; then |
|
die "aborted" |
|
fi |
|
|
|
mkdir -p "${CONFIG_DIR}" |
|
{ |
|
echo '# Generated by setup-gclaude.sh — https://code.claude.com/docs/en/google-vertex-ai' |
|
printf 'ANTHROPIC_VERTEX_PROJECT_ID=%q\n' "${PROJECT_ID}" |
|
printf 'GOOGLE_APPLICATION_CREDENTIALS=%q\n' "${KEY_FILE}" |
|
printf 'CLOUD_ML_REGION=%q\n' "${REGION}" |
|
} >"${ENV_FILE}" |
|
chmod 600 "${ENV_FILE}" |
|
|
|
if [[ -f "${GCLAUDE_BIN}" ]] && ! prompt_yes "Overwrite existing ${GCLAUDE_BIN}?"; then |
|
die "aborted before installing gclaude" |
|
fi |
|
|
|
mkdir -p "${LOCAL_BIN}" |
|
cat >"${GCLAUDE_BIN}" <<'GCLAUDE' |
|
#!/usr/bin/env bash |
|
# Thin wrapper: run Claude Code against Vertex AI with sane defaults. |
|
# Docs: https://code.claude.com/docs/en/google-vertex-ai |
|
set -euo pipefail |
|
|
|
gclaude_load_env_file() { |
|
local path="$1" |
|
if [[ -f "$path" ]]; then |
|
set -a |
|
# shellcheck disable=SC1090 |
|
source "$path" |
|
set +a |
|
fi |
|
} |
|
|
|
if [[ -n "${GCLAUDE_ENV_FILE:-}" ]]; then |
|
gclaude_load_env_file "${GCLAUDE_ENV_FILE}" |
|
else |
|
gclaude_load_env_file "${XDG_CONFIG_HOME:-$HOME/.config}/gclaude/env" |
|
fi |
|
|
|
export CLAUDE_CODE_USE_VERTEX=1 |
|
export CLOUD_ML_REGION="${CLOUD_ML_REGION:-global}" |
|
|
|
if [[ -n "${GCLAUDE_GOOGLE_APPLICATION_CREDENTIALS:-}" ]]; then |
|
export GOOGLE_APPLICATION_CREDENTIALS="${GCLAUDE_GOOGLE_APPLICATION_CREDENTIALS}" |
|
fi |
|
|
|
CLAUDE_BIN="${GCLAUDE_CLAUDE_BIN:-claude}" |
|
|
|
if ! command -v "${CLAUDE_BIN}" >/dev/null 2>&1; then |
|
echo "gclaude: '${CLAUDE_BIN}' not found on PATH. Install Claude Code or set GCLAUDE_CLAUDE_BIN." >&2 |
|
exit 127 |
|
fi |
|
|
|
if [[ -z "${ANTHROPIC_VERTEX_PROJECT_ID:-}" ]]; then |
|
echo "gclaude: set ANTHROPIC_VERTEX_PROJECT_ID (GCP project ID)." >&2 |
|
echo " Tip: run setup-gclaude.sh or edit ~/.config/gclaude/env" >&2 |
|
exit 1 |
|
fi |
|
|
|
if [[ -z "${GOOGLE_APPLICATION_CREDENTIALS:-}" ]]; then |
|
echo "gclaude: set GOOGLE_APPLICATION_CREDENTIALS to your service account JSON key path," >&2 |
|
echo " or set GCLAUDE_GOOGLE_APPLICATION_CREDENTIALS to the same path." >&2 |
|
exit 1 |
|
fi |
|
|
|
if [[ ! -f "${GOOGLE_APPLICATION_CREDENTIALS}" ]]; then |
|
echo "gclaude: credentials file not found: ${GOOGLE_APPLICATION_CREDENTIALS}" >&2 |
|
exit 1 |
|
fi |
|
|
|
exec "${CLAUDE_BIN}" "$@" |
|
GCLAUDE |
|
chmod +x "${GCLAUDE_BIN}" |
|
|
|
echo |
|
echo "Done." |
|
echo " Config: ${ENV_FILE}" |
|
echo " Key: ${KEY_FILE}" |
|
echo " CLI: ${GCLAUDE_BIN}" |
|
echo |
|
if [[ ":${PATH}:" != *":${LOCAL_BIN}:"* ]]; then |
|
echo "Add ~/.local/bin to PATH, e.g. in ~/.zshrc:" |
|
echo " export PATH=\"\${HOME}/.local/bin:\${PATH}\"" |
|
echo |
|
fi |
|
echo "Run: gclaude (same arguments as claude)" |