flake/modules/home/terminal/hr/hr.nu
2026-03-10 10:13:21 +00:00

474 lines
13 KiB
Text

# 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 <command>"
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<string>] {
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<string>] {
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<string>] {
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<string>] {
if ($args | is-empty) {
print "Usage: hr call <route-name>[/path] [OPTIONS]"
print ""
print "Options:"
print " -key value Set JSON field (supports dot notation, e.g., -items.0.type \"trip\")"
print " --data <record> 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<string>] {
if ($args | is-empty) {
print "Usage: hr cf <function-name> [OPTIONS]"
print ""
print "Options:"
print " -key value Set JSON field (supports dot notation, e.g., -items.0.type \"trip\")"
print " --data <record> 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 }
}
}