# Set default PROJECT_ID if not already set if [[ -z "$PROJECT_ID" ]]; then export PROJECT_ID="mk2-test" fi _hr_usage() { echo "Usage: hr " echo "Commands:" echo " switch Switch PROJECT_ID between mk2-test and mk2-prod" echo " call Call a Cloud Run service route" echo " cf Call a Cloud Function" echo " init py Initialize a python devenv environment (git-ignored)" echo " init go Initialize a go devenv environment (git-ignored)" echo " freeze Freeze dependencies to requirements.txt" } _hr_init_devenv() { if [ -f .gitignore ]; then cp .gitignore .gitignore.bak fi if command -v devenv >/dev/null; then devenv init echo "Direnv allowed" else echo "Error: devenv not found in path." return 1 fi if [ -f .gitignore.bak ]; then mv .gitignore.bak .gitignore elif [ -f .gitignore ]; then rm .gitignore fi } _hr_add_ignores() { if git rev-parse --git-dir >/dev/null 2>&1; then EXCLUDE_FILE=$(git rev-parse --git-path info/exclude) mkdir -p "$(dirname "$EXCLUDE_FILE")" for file in "$@"; do if ! grep -Fxq "$file" "$EXCLUDE_FILE" 2>/dev/null; then echo "$file" >>"$EXCLUDE_FILE" echo "Added $file to local git exclude ($EXCLUDE_FILE)" fi done else echo "Warning: Not a git repository. Skipping git ignore setup." fi } _hr_py_files() { cat <devenv.nix {pkgs, ...}: { packages = [ pkgs.google-cloud-sdk pkgs.libpq ]; languages.python = { enable = true; venv.enable = true; uv = { enable = true; sync.enable = false; }; }; # We use the named index "google" defined in uv.toml env.UV_INDEX_GOOGLE_USERNAME = "oauth2accesstoken"; env.PROJECT_ID = "mk2-test"; enterShell = '' if ! gcloud auth print-access-token >/dev/null 2>&1; then echo "⚠️ gcloud not authenticated. Run 'gcloud auth login' to access Google Artifact Registry." else export UV_INDEX_GOOGLE_PASSWORD=\$(gcloud auth print-access-token) fi uv sync ''; } EOF cat <uv.toml [[index]] name = "google" url = "https://europe-west1-python.pkg.dev/mk2-prod/python-packages/simple/" EOF } _hr_rs_files() { cat <devenv.nix {pkgs, ...}: { languages.rust = { enable = true; channel = "stable"; }; } EOF cat <devenv.yaml inputs: rust-overlay: url: github:oxalica/rust-overlay inputs: nixpkgs: follows: nixpkgs EOF } _hr_cpp_files() { cat <devenv.nix { pkgs, ... }: let # Use glibc-compatible static openssl to match system libs staticOpenSSL = pkgs.openssl.override { static = true; }; # Shim to satisfy CMake looking for "ssl.a" compatOpenSSL = pkgs.runCommand "openssl-compat" {} '' mkdir -p \$out/lib ln -s \${staticOpenSSL.out}/lib/libssl.a \$out/lib/ssl.a ln -s \${staticOpenSSL.out}/lib/libcrypto.a \$out/lib/crypto.a ''; in { packages = [ pkgs.cmake pkgs.clang-tools pkgs.pkg-config pkgs.mosquitto staticOpenSSL compatOpenSSL ]; # Explicitly add lib paths so linker finds -lssl AND ssl.a env.LIBRARY_PATH = "\${staticOpenSSL.out}/lib:\${compatOpenSSL}/lib"; env.CPATH = "\${staticOpenSSL.dev}/include"; languages.cplusplus.enable = true; } EOF } _hr_go_files() { cat <devenv.nix {pkgs, ...}: { languages.go = { enable = true; }; } EOF } _hr_init_base() { local name="$1" local func="$2" shift 2 local ignores=("$@") echo "Initializing $name devenv..." # 1. Init devenv _hr_init_devenv # 2. Replace devenv.nix "$func" # 3. Add to local git exclude IGNORES=( ".devenv*" ".direnv" "devenv.nix" "devenv.yaml" "devenv.lock" ".envrc" "${ignores[@]}" ) _hr_add_ignores "${IGNORES[@]}" direnv allow } _hr_init_py() { IGNORES=( "uv.lock" "uv.toml" ) _hr_init_base "Python" _hr_py_files "${IGNORES[@]}" } _hr_init_rs() { _hr_init_base "Rust" _hr_rs_files } _hr_init_cpp() { _hr_init_base "C++" _hr_cpp_files mkdir -p build && cd build && cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_BUILD_TYPE=Release .. && make -j$(nproc) && cp compile_commands.json .. } _hr_init_go() { _hr_init_base "Go" _hr_go_files } _hr_freeze() { local extra_index_url="https://europe-west1-python.pkg.dev/mk2-prod/python-packages/simple/" # Install the auth plugin and keyring CLI uv pip install keyrings.google-artifactregistry-auth==1.1.2 keyring # Install project dependencies using the subprocess keyring provider uv pip install --no-cache -e ".[test]" --extra-index-url "${extra_index_url}" --keyring-provider subprocess # Generate requirements.txt echo "--extra-index-url ${extra_index_url}" >requirements.txt uv pip freeze --exclude-editable >>requirements.txt } _hr_add_json_field() { local json="$1" local key="$2" local value="$3" local jq_opt="--arg" # Check if explicit boolean if [[ "$value" == "true" || "$value" == "false" ]]; then jq_opt="--argjson" # Check if number (integer or float, no leading zeros unless just 0) elif [[ "$value" =~ ^-?(0|[1-9][0-9]*)(\.[0-9]+)?$ ]]; then jq_opt="--argjson" # Check if object or array elif [[ "$value" == "["* || "$value" == "{"* ]]; then if echo "$value" | jq empty >/dev/null 2>&1; then jq_opt="--argjson" else # Warn to stderr, but proceed as string echo "Warning: Value for '$key' looks like JSON but is invalid. Treating as string." >&2 fi fi # Apply the value at the path defined by the key (dot-notation supported) # paths like items.0.id are converted to ["items", 0, "id"] echo "$json" | jq --arg k "$key" $jq_opt v "$value" \ 'setpath($k | split(".") | map(if test("^[0-9]+$") then tonumber else . end); $v)' } _hr_call() { local route_arg="$1" shift if [[ -z "$route_arg" ]]; then echo "Usage: hr call [/path] " return 1 fi local service_name local url_path if [[ "$route_arg" == */* ]]; then service_name="${route_arg%%/*}" url_path="/${route_arg#*/}" else service_name="$route_arg" url_path="" fi local project_number if [[ "$PROJECT_ID" == "mk2-prod" ]]; then project_number="1013087376822" else project_number="322048751601" fi if ! command -v jq >/dev/null; then echo "Error: jq is required but not installed." return 1 fi local json_payload="{}" while [[ $# -gt 0 ]]; do if [[ "$1" == -* ]]; then local key="${1#-}" if [[ -z "$2" || "$2" == -* ]]; then echo "Error: Missing value for option $key" return 1 fi local value="$2" json_payload=$(_hr_add_json_field "$json_payload" "$key" "$value") shift 2 else echo "Error: Unexpected argument '$1'" return 1 fi done local url="https://${service_name}-${project_number}.europe-west1.run.app${url_path}" echo "Calling $url..." echo "$json_payload" curl "$url" \ -H "Authorization: bearer $(gcloud auth print-identity-token)" \ -H "Content-Type: application/json" \ -d "$json_payload" } _hr_cf() { local function_name="$1" shift if [[ -z "$function_name" ]]; then echo "Usage: hr cf " return 1 fi if ! command -v jq >/dev/null; then echo "Error: jq is required but not installed." return 1 fi local json_payload="{}" while [[ $# -gt 0 ]]; do if [[ "$1" == -* ]]; then local key="${1#-}" if [[ -z "$2" || "$2" == -* ]]; then echo "Error: Missing value for option $key" return 1 fi local value="$2" json_payload=$(_hr_add_json_field "$json_payload" "$key" "$value") shift 2 else echo "Error: Unexpected argument '$1'" return 1 fi done local url="https://europe-west1-${PROJECT_ID}.cloudfunctions.net/${function_name}" echo "Calling $url..." echo "$json_payload" curl "$url" \ -H "Authorization: bearer $(gcloud auth print-identity-token)" \ -H "Content-Type: application/json" \ -d "$json_payload" } hr() { if [[ $# -eq 0 ]]; then _hr_usage return 1 fi local command="$1" shift if [[ "$command" == "switch" ]]; then if [[ -z "$1" ]]; then # Toggle between test and prod if [[ "$PROJECT_ID" == "mk2-test" ]]; then export PROJECT_ID="mk2-prod" echo "Switched PROJECT_ID to mk2-prod" else export PROJECT_ID="mk2-test" echo "Switched PROJECT_ID to mk2-test" fi elif [[ "$1" == "test" ]]; then export PROJECT_ID="mk2-test" echo "Set PROJECT_ID to mk2-test" elif [[ "$1" == "prod" ]]; then export PROJECT_ID="mk2-prod" echo "Set PROJECT_ID to mk2-prod" else echo "Usage: hr switch [test|prod]" return 1 fi return 0 fi # Run original logic in a subshell to preserve set -e behavior without affecting current shell ( set -e # Restore arguments for processing set -- "$command" "$@" if [ "$1" = "init" ] && [ "$2" = "py" ]; then _hr_init_py elif [ "$1" = "init" ] && [ "$2" = "rs" ]; then _hr_init_rs elif [ "$1" = "init" ] && [ "$2" = "go" ]; then _hr_init_go elif [ "$1" = "init" ] && [ "$2" = "cpp" ]; then _hr_init_cpp elif [ "$1" = "freeze" ]; then _hr_freeze elif [ "$1" = "call" ]; then shift _hr_call "$@" elif [ "$1" = "cf" ]; then shift _hr_cf "$@" else _hr_usage exit 1 fi ) }