# HR - Hefring work tooling for nushell # # This module provides commands for working with Hefring Cloud Run services and Cloud Functions. # # Key features: # - Shell-style argument passing: -key value supports dot notation for nested structures # Example: hr call service -items.0.type "trip" -items.0.id "123" # # - Nushell record syntax with --data flag for complex payloads: # Example: hr call service --data {company_id: "abc", items: [{type: "trip"}]} # # - Automatic type detection: numbers, booleans, and JSON are parsed automatically # Example: -count "42" → number, -active "true" → boolean # # - Pretty display: Payloads are shown as Nushell tables before sending def _hr_usage [] { print "Usage: hr " print "Commands:" print " switch Switch PROJECT_ID between mk2-test and mk2-prod" print " call Call a Cloud Run service route" print " cf Call a Cloud Function" print " init py Initialize a python devenv environment (git-ignored)" print " init go Initialize a go devenv environment (git-ignored)" print " init rs Initialize a rust devenv environment (git-ignored)" print " init cpp Initialize a C++ devenv environment (git-ignored)" print " freeze Freeze dependencies to requirements.txt" } def _hr_init_devenv [] { if (".gitignore" | path exists) { cp .gitignore .gitignore.bak } if (which devenv | is-empty) { error make { msg: "Error: devenv not found in path." } } devenv init print "Direnv allowed" if (".gitignore.bak" | path exists) { mv .gitignore.bak .gitignore } else if (".gitignore" | path exists) { rm .gitignore } } def _hr_add_ignores [files: list] { let git_dir = (do { git rev-parse --git-dir } | complete) if $git_dir.exit_code != 0 { print "Warning: Not a git repository. Skipping git ignore setup." return } let exclude_file = (git rev-parse --git-path info/exclude) mkdir (($exclude_file) | path dirname) for file in $files { let already_exists = ( if ($exclude_file | path exists) { open $exclude_file | lines | any { |line| $line == $file } } else { false } ) if not $already_exists { $"($file)\n" | save --append $exclude_file print $"Added ($file) to local git exclude \(($exclude_file)\)" } } } def _hr_py_files [] { " {pkgs, ...}: { packages = [ pkgs.google-cloud-sdk pkgs.libpq ]; languages.python = { enable = true; venv.enable = true; uv = { enable = true; sync = { enable = true; allExtras = true; }; }; }; # We use the named index \"google\" defined in uv.toml env.UV_INDEX_GOOGLE_USERNAME = \"oauth2accesstoken\"; env.PROJECT_ID = \"mk2-test\"; enterShell = '' export PATH=\"$DEVENV_STATE/venv/bin:$PATH\" 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 ''; } " | save -f devenv.nix " [[index]] name = \"google\" url = \"https://europe-west1-python.pkg.dev/mk2-prod/python-packages/simple/\" " | save -f uv.toml } def _hr_rs_files [] { " {pkgs, ...}: { languages.rust = { enable = true; channel = \"stable\"; }; } " | save -f devenv.nix " inputs: rust-overlay: url: github:oxalica/rust-overlay inputs: nixpkgs: follows: nixpkgs " | save -f devenv.yaml } def _hr_cpp_files [] { " { 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; } " | save -f devenv.nix } def _hr_go_files [] { " {pkgs, ...}: { languages.go = { enable = true; }; } " | save -f devenv.nix } def _hr_init_base [name: string, write_files: closure, ignores: list] { print $"Initializing ($name) devenv..." # 1. Init devenv _hr_init_devenv # 2. Write language-specific files do $write_files # 3. Add to local git exclude let base_ignores = [ ".devenv*" ".direnv" "devenv.nix" "devenv.yaml" "devenv.lock" ".envrc" ] _hr_add_ignores ($base_ignores ++ $ignores) direnv allow } def _hr_setpath [data: any, path: list, value: any] { # Recursively build nested structure, similar to jq's setpath if ($path | is-empty) { $value } else if ($path | length) == 1 { $data | upsert ($path | first) $value } else { let key = ($path | first) let rest = ($path | skip 1) let next_key = ($rest | first) # Check if key exists in data and get intermediate value let key_type = ($key | describe) let data_type = ($data | describe) let is_list = ($data_type | str starts-with "list") or ($data_type | str starts-with "table") # Determine if key exists and get intermediate structure let intermediate = if $key_type == "int" and $is_list { # Array index if $key < ($data | length) { $data | get $key } else { # Index doesn't exist, create structure based on next key if ($next_key | describe) == "int" { [] } else { {} } } } else if $key_type != "int" { # Object key if $key in $data { $data | get $key } else { # Key doesn't exist, create structure based on next key if ($next_key | describe) == "int" { [] } else { {} } } } else { # Data is not a list but key is int - create array if ($next_key | describe) == "int" { [] } else { {} } } # Recursively set the rest of the path let updated_intermediate = (_hr_setpath $intermediate $rest $value) # Update data with the new intermediate value $data | upsert $key $updated_intermediate } } def _hr_add_json_field [json: string, key: string, value: string] { # Determine if value should be parsed as raw JSON or treated as a string let is_bool = ($value == "true" or $value == "false") let is_number = ($value =~ '^-?(0|[1-9][0-9]*)(\.[0-9]+)?$') let is_json_container = ($value =~ '^\[' or $value =~ '^\{') # Parse the value into the appropriate type let parsed_value = if $is_bool or $is_number { $value | from json } else if $is_json_container { let parsed = (do { $value | from json } | complete) if $parsed.exit_code == 0 { $value | from json } else { print $"Warning: Value for '($key)' looks like JSON but is invalid. Treating as string." --stderr $value } } else { $value } # Convert key path (e.g., "items.0.type") to a list of path segments # Numbers are converted to integers for array indexing let path_segments = ($key | split row "." | each { |segment| if ($segment =~ '^[0-9]+$') { $segment | into int } else { $segment } }) let data = ($json | from json) _hr_setpath $data $path_segments $parsed_value | to json } def _hr_parse_flags [args: list] { mut json_str = "{}" mut i = 0 while $i < ($args | length) { let arg = ($args | get $i) if ($arg | str starts-with "-") { let key = ($arg | str replace --regex '^-+' '') let next_i = $i + 1 if $next_i >= ($args | length) { error make { msg: $"Error: Missing value for option ($key)" } } let next = ($args | get $next_i) if ($next | str starts-with "-") { error make { msg: $"Error: Missing value for option ($key)" } } $json_str = (_hr_add_json_field $json_str $key $next) $i = $i + 2 } else { error make { msg: $"Error: Unexpected argument '($arg)'" } } } $json_str | from json } def _hr_call [args: list] { if ($args | is-empty) { print "Usage: hr call [/path] [OPTIONS]" print "" print "Options:" print " -key value Set JSON field (supports dot notation, e.g., -items.0.type \"trip\")" print " --data Pass structured data as a Nushell record" return } let route_arg = ($args | first) let rest = ($args | skip 1) let parts = if ($route_arg | str contains "/") { let service = ($route_arg | split row "/" | first) let path = "/" + ($route_arg | split row "/" | skip 1 | str join "/") { service: $service, path: $path } } else { { service: $route_arg, path: "" } } let project_number = if $env.PROJECT_ID == "mk2-prod" { "1013087376822" } else { "322048751601" } # Check if --data flag is present let data_idx = ($rest | enumerate | where item == "--data" | get index.0? | default (-1)) let payload_record = if $data_idx >= 0 { # Use --data record if ($data_idx + 1) >= ($rest | length) { error make { msg: "Error: --data requires a record argument" } } let data_str = ($rest | get ($data_idx + 1)) # Try to parse as Nushell code to get a record try { nu -c $data_str } catch { error make { msg: $"Error: --data argument must be a valid Nushell record, got: ($data_str)" } } } else { # Use -key value pairs _hr_parse_flags $rest } let payload = ($payload_record | to json -r) let url = $"https://($parts.service)-($project_number).europe-west1.run.app($parts.path)" print $"Calling ($url)..." print ($payload | from json) let token = (gcloud auth print-identity-token) ^curl -s -S -L $url -H $"Authorization: Bearer ($token)" -H "Content-Type: application/json" -d $payload } def _hr_cf [args: list] { if ($args | is-empty) { print "Usage: hr cf [OPTIONS]" print "" print "Options:" print " -key value Set JSON field (supports dot notation, e.g., -items.0.type \"trip\")" print " --data Pass structured data as a Nushell record" return } let function_name = ($args | first) let rest = ($args | skip 1) # Check if --data flag is present let data_idx = ($rest | enumerate | where item == "--data" | get index.0? | default (-1)) let payload_record = if $data_idx >= 0 { # Use --data record if ($data_idx + 1) >= ($rest | length) { error make { msg: "Error: --data requires a record argument" } } let data_str = ($rest | get ($data_idx + 1)) # Try to parse as Nushell code to get a record try { nu -c $data_str } catch { error make { msg: $"Error: --data argument must be a valid Nushell record, got: ($data_str)" } } } else { # Use -key value pairs _hr_parse_flags $rest } let payload = ($payload_record | to json -r) let url = $"https://europe-west1-($env.PROJECT_ID).cloudfunctions.net/($function_name)" print $"Calling ($url)..." print ($payload | from json) let token = (gcloud auth print-identity-token) ^curl -s -S -L $url -H $"Authorization: Bearer ($token)" -H "Content-Type: application/json" -d $payload } # HR - Hefring work tooling export def --env --wrapped hr [...args: string] { if ($args | is-empty) { _hr_usage return } let cmd = ($args | first) let rest = ($args | skip 1) match $cmd { "switch" => { if ($rest | is-empty) { if $env.PROJECT_ID == "mk2-test" { $env.PROJECT_ID = "mk2-prod" print "Switched PROJECT_ID to mk2-prod" } else { $env.PROJECT_ID = "mk2-test" print "Switched PROJECT_ID to mk2-test" } } else { match ($rest | first) { "test" => { $env.PROJECT_ID = "mk2-test" print "Set PROJECT_ID to mk2-test" } "prod" => { $env.PROJECT_ID = "mk2-prod" print "Set PROJECT_ID to mk2-prod" } _ => { print "Usage: hr switch [test|prod]" } } } } "init" => { if ($rest | is-empty) { _hr_usage return } match ($rest | first) { "py" => { _hr_init_base "Python" { _hr_py_files } ["uv.lock" "uv.toml"] } "rs" => { _hr_init_base "Rust" { _hr_rs_files } [] } "go" => { _hr_init_base "Go" { _hr_go_files } [] } "cpp" => { _hr_init_base "C++" { _hr_cpp_files } [] mkdir build cd build cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_BUILD_TYPE=Release .. make -j (sys cpu | length) cp compile_commands.json .. } _ => { _hr_usage } } } "freeze" => { let extra_index_url = "https://europe-west1-python.pkg.dev/mk2-prod/python-packages/simple/" uv pip install keyrings.google-artifactregistry-auth==1.1.2 keyring uv pip install --no-cache -e ".[test]" --extra-index-url $extra_index_url --keyring-provider subprocess $"--extra-index-url ($extra_index_url)\n" | save -f requirements.txt uv pip freeze --exclude-editable | save --append requirements.txt } "call" => { _hr_call $rest } "cf" => { _hr_cf $rest } _ => { _hr_usage } } }