diff --git a/modules/home/terminal/hr/default.nix b/modules/home/terminal/hr/default.nix index 53a6ea6..e8446f6 100644 --- a/modules/home/terminal/hr/default.nix +++ b/modules/home/terminal/hr/default.nix @@ -12,9 +12,16 @@ in { options.mods.terminal.hr.enable = lib.mkEnableOption "Hefring (Work Tooling)"; config = lib.mkIf cfg.hr.enable { + programs.nushell = lib.mkIf cfg.nushell.enable { + extraConfig = '' + $env.PROJECT_ID = if ($env | get -o PROJECT_ID | is-empty) { "mk2-test" } else { $env.PROJECT_ID } + '' + builtins.readFile ./hr.nu; + }; + programs.starship.settings.custom.project_id = { - command = "if echo \"$PROJECT_ID\" | grep -q \"prod\"; then printf \"\\033[1;33m \\033[1;34m$PROJECT_ID\\033[0m\"; else printf \"\\033[1;34m$PROJECT_ID\\033[0m\"; fi"; - when = "test -n \"$PROJECT_ID\""; + command = "if $env.PROJECT_ID =~ 'prod' { $'(ansi yellow_bold) (ansi blue_bold)($env.PROJECT_ID)(ansi reset)' } else { $'(ansi blue_bold)($env.PROJECT_ID)(ansi reset)' }"; + when = "not ($env | get -o PROJECT_ID | is-empty)"; + shell = ["nu" "--no-config-file" "-c"]; format = "on $output "; }; diff --git a/modules/home/terminal/hr/hr.nu b/modules/home/terminal/hr/hr.nu new file mode 100644 index 0000000..c02f758 --- /dev/null +++ b/modules/home/terminal/hr/hr.nu @@ -0,0 +1,344 @@ +# HR - Hefring work tooling for nushell + +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_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 =~ '^\{') + + if $is_bool or $is_number { + $json | from json | upsert $key ($value | from json) | to json + } else if $is_json_container { + let parsed = (do { $value | from json } | complete) + if $parsed.exit_code == 0 { + $json | from json | upsert $key ($value | from json) | to json + } else { + print $"Warning: Value for '($key)' looks like JSON but is invalid. Treating as string." --stderr + $json | from json | upsert $key $value | to json + } + } else { + $json | from json | upsert $key $value | to json + } +} + +def _hr_parse_flags [args: list] { + mut payload = {} + 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)" } + } + $payload = ($payload | upsert $key $next) + $i = $i + 2 + } else { + error make { msg: $"Error: Unexpected argument '($arg)'" } + } + } + $payload +} + +def _hr_call [route_arg: string, ...rest: string] { + 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" + } + + let payload = (_hr_parse_flags $rest | to json) + let url = $"https://($parts.service)-($project_number).europe-west1.run.app($parts.path)" + + print $"Calling ($url)..." + print $payload + + let token = (gcloud auth print-identity-token) + http post $url $payload --content-type "application/json" --headers { Authorization: $"bearer ($token)" } +} + +def _hr_cf [function_name: string, ...rest: string] { + let payload = (_hr_parse_flags $rest | to json) + let url = $"https://europe-west1-($env.PROJECT_ID).cloudfunctions.net/($function_name)" + + print $"Calling ($url)..." + print $payload + + let token = (gcloud auth print-identity-token) + http post $url $payload --content-type "application/json" --headers { Authorization: $"bearer ($token)" } +} + +# HR - Hefring work tooling +export def 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" => { + if ($rest | is-empty) { + print "Usage: hr call [/path] " + return + } + _hr_call ($rest | first) ...($rest | skip 1) + } + "cf" => { + if ($rest | is-empty) { + print "Usage: hr cf " + return + } + _hr_cf ($rest | first) ...($rest | skip 1) + } + _ => { _hr_usage } + } +} diff --git a/modules/home/terminal/hr/hr.sh b/modules/home/terminal/hr/hr.sh index 4104be9..83ab944 100644 --- a/modules/home/terminal/hr/hr.sh +++ b/modules/home/terminal/hr/hr.sh @@ -60,7 +60,10 @@ _hr_py_files() { venv.enable = true; uv = { enable = true; - sync.enable = false; + sync = { + enable = true; + allExtras = true; + }; }; }; @@ -69,12 +72,12 @@ _hr_py_files() { 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 - uv sync ''; } EOF