diff --git a/Makefile b/Makefile index 329cb9f32..531e7bc76 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,9 @@ NIX_TRUSTED_KEYS := cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShj NIX_CACHIX_CONF := /etc/nix/cachix.conf # Check if user is trusted (to avoid "ignoring untrusted substituter" warnings) NIX_USER_TRUSTED := $(shell grep -qE "trusted-users.*=.*(\\*|$(shell whoami))" /etc/nix/nix.conf 2>/dev/null && echo "yes" || echo "no") +NAMED_HOSTS := galactica kyber matic viper +NIXOS_NAMED_HOSTS := $(filter matic viper,$(NAMED_HOSTS)) +ISO_NAMED_HOSTS := $(filter matic viper,$(NAMED_HOSTS)) # Nix configuration system NIX_SYSTEM := $(shell if [ "$(OS)" = "Darwin" ] && [ "$(ARCH)" = "arm64" ]; then \ @@ -137,7 +140,7 @@ default: help ## Default target (shows help). help: ## Show this help message. @echo "Usage: make " @echo - @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z0-9_-]+:.*?## / {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z0-9_%-]+:.*?## / {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) ##@ General @@ -638,6 +641,21 @@ nix-setup-offline: ## Set up offline environment. ##@ Named Hosts Specific Targets +define MAKE_NIXOS_HOST_BUILD_TARGET +.PHONY: build-$(1) +build-$(1): ## Build the $(1) named host configuration. + @$(MAKE) build HOST=$(1) +endef + +define MAKE_NIXOS_HOST_ISO_TARGET +.PHONY: build-$(1)-iso +build-$(1)-iso: ## Build the $(1) named host live/install ISO. + @$(MAKE) build-iso HOST=$(1) +endef + +$(foreach host,$(NIXOS_NAMED_HOSTS),$(eval $(call MAKE_NIXOS_HOST_BUILD_TARGET,$(host)))) +$(foreach host,$(ISO_NAMED_HOSTS),$(eval $(call MAKE_NIXOS_HOST_ISO_TARGET,$(host)))) + .PHONY: build-vm build-vm: ## Build a named host VM launcher (set HOST=, e.g. make build-vm HOST=viper). @host="$(HOST)"; \ @@ -671,7 +689,8 @@ run-vm: ## Run a named host VM launcher (set HOST=, e.g. make run-vm HOST= .PHONY: build-iso build-iso: ## Build a named host live/install ISO and copy it to ./.iso (set HOST=, e.g. make build-iso HOST=viper). - @host="$(HOST)"; \ + @set -e; \ + host="$(HOST)"; \ if [ -z "$$host" ]; then \ host="$(DETECTED_HOST)"; \ fi; \ @@ -681,7 +700,7 @@ build-iso: ## Build a named host live/install ISO and copy it to ./.iso (s fi; \ echo "💿 Building ISO for $$host"; \ $(NIX_ALLOW_UNFREE) $(NIX_EXEC) build .#nixosConfigurations.$$host"Iso".config.system.build.isoImage $(NIX_FLAGS) --impure --show-trace; \ - iso_path=$$(find ./result -type f -name '*.iso' | head -n 1); \ + iso_path=$$(bash ./scripts/find-built-iso.sh ./result); \ if [ -z "$$iso_path" ]; then \ echo "❌ Could not find a built ISO under ./result"; \ exit 1; \ diff --git a/bun.lock b/bun.lock index e6fa10779..3507b3b4d 100644 --- a/bun.lock +++ b/bun.lock @@ -94,8 +94,9 @@ "@google/gemini-cli", "mcporter", "paperclipai", - "vite-plus", + "takt", "@traces-sh/traces", + "vite-plus", "@oh-my-pi/swarm-extension", "turbo", "@google/jules", diff --git a/config/hyprland/default.nix b/config/hyprland/default.nix index b23c7253b..cbbb87cb3 100644 --- a/config/hyprland/default.nix +++ b/config/hyprland/default.nix @@ -10,7 +10,7 @@ extraConfig = '' exec-once = ${pkgs.hyprpanel}/bin/hyprpanel exec-once = ${pkgs.hyprshell}/bin/hyprshell run & - exec-once = ${pkgs.lib.getExe pkgs.hyprpolkitagent} + exec-once = ${pkgs.hyprpolkitagent}/libexec/hyprpolkitagent exec-once = sleep 3 && ${pkgs.eww}/bin/eww open clock-widget exec-once = ${pkgs.nwg-dock-hyprland}/bin/nwg-dock-hyprland -d -i 48 -hd 0 '' diff --git a/config/rofi/default.nix b/config/rofi/default.nix index 5f52b0bf7..cda08f541 100644 --- a/config/rofi/default.nix +++ b/config/rofi/default.nix @@ -119,5 +119,15 @@ in ]; noDisplay = false; }; + battery-settings = { + name = "Battery Settings"; + exec = "auto-cpufreq-gtk"; + icon = "battery"; + categories = [ + "System" + "Settings" + ]; + noDisplay = false; + }; }; } diff --git a/home-manager/modules/uv-globals/install-uv-globals.sh b/home-manager/modules/uv-globals/install-uv-globals.sh index 7375e9ad2..d0b95ccbc 100755 --- a/home-manager/modules/uv-globals/install-uv-globals.sh +++ b/home-manager/modules/uv-globals/install-uv-globals.sh @@ -59,11 +59,38 @@ echo "$DEPS" | while read -r pkg; do if [ -n "$installed_version" ] && [ -n "$req_version" ]; then if printf '%s\n%s\n' "$req_version" "$installed_version" | sort -V | head -n1 | grep -qx "$req_version"; then echo "$name $installed_version already installed, skipping" - continue + else + echo "Installing $pkg..." + uv tool install "$pkg" --python "$PYTHON_VERSION" --force 2>/dev/null || echo "Failed to install $pkg, skipping..." fi + else + echo "Installing $pkg..." + uv tool install "$pkg" --python "$PYTHON_VERSION" --force 2>/dev/null || echo "Failed to install $pkg, skipping..." fi - echo "Installing $pkg..." - uv tool install "$pkg" --python "$PYTHON_VERSION" --force 2>/dev/null || echo "Failed to install $pkg, skipping..." + + # Symlink each tool's python3 for per-tool access: `python3- -m ` + tool_python="${HOME}/.local/share/uv/tools/${name}/bin/python3" + if [ -f "$tool_python" ]; then + ln -sf "$tool_python" "${HOME}/.local/bin/python3-${name}" + fi +done + +# Write a dispatcher so `python3 -m ` uses that tool's isolated Python +cat > "${HOME}/.local/bin/python3" << 'EOF' +#!/usr/bin/env bash +prev="" +for arg in "$@"; do + if [ "$prev" = "-m" ]; then + per_tool="${HOME}/.local/bin/python3-${arg}" + if [ -x "$per_tool" ]; then + exec "$per_tool" "$@" + fi + break + fi + prev="$arg" done +exec /etc/profiles/per-user/"${USER}"/bin/python3 "$@" +EOF +chmod +x "${HOME}/.local/bin/python3" echo "uv globals installation complete" diff --git a/home-manager/programs/fish/functions/_pixelh_function.fish b/home-manager/programs/fish/functions/_pixelh_function.fish index e69de29bb..9f707d026 100644 --- a/home-manager/programs/fish/functions/_pixelh_function.fish +++ b/home-manager/programs/fish/functions/_pixelh_function.fish @@ -0,0 +1,18 @@ +function _pixelh_function --description "Run Pi headlessly with the local Qwen model" + # Prompt for input and run Pi in print mode with the local Qwen model + # Usage: pixelh + + set -l prompt + if test (count $argv) -gt 0 + set prompt (string join " " $argv) + else + read -P "Prompt: " prompt + end + + if test -z "$prompt" + echo "No prompt provided, aborting." >&2 + return 1 + end + + pi --model 'lmstudio/qwen3.5-0.8b-optiq' -p "$prompt" +end diff --git a/home-manager/services/make-updater/default.nix b/home-manager/services/make-updater/default.nix index 418f62986..85d074f49 100644 --- a/home-manager/services/make-updater/default.nix +++ b/home-manager/services/make-updater/default.nix @@ -17,6 +17,7 @@ in pkgs.bash pkgs.coreutils pkgs.gnumake + pkgs.jq ] }:/opt/homebrew/bin:/usr/local/bin"; }; @@ -49,6 +50,7 @@ in pkgs.gnumake pkgs.gnused pkgs.go + pkgs.jq pkgs.libtool pkgs.neovim pkgs.nix diff --git a/named-hosts/matic/default.nix b/named-hosts/matic/default.nix index 9e301141b..d14ec15d2 100644 --- a/named-hosts/matic/default.nix +++ b/named-hosts/matic/default.nix @@ -195,6 +195,9 @@ import ../../hosts/nixos { security.pam.services.sudo = { fprintAuth = true; }; + security.pam.services.polkit-1 = { + fprintAuth = true; + }; # Firmware updates services.fwupd.enable = true; @@ -217,6 +220,10 @@ import ../../hosts/nixos { }; }; }; + # auto-cpufreq-gtk writes turbo overrides to /opt/auto-cpufreq/ + systemd.tmpfiles.rules = [ + "d /opt/auto-cpufreq 0755 root root -" + ]; # Power button behavior - lock screen instead of shutdown services.logind.settings.Login.HandlePowerKey = "lock"; diff --git a/named-hosts/matic/falcon.nix b/named-hosts/matic/falcon.nix index 7a74e6930..3ba37a448 100644 --- a/named-hosts/matic/falcon.nix +++ b/named-hosts/matic/falcon.nix @@ -30,8 +30,15 @@ in systemd.services.falcon-sensor = { description = "CrowdStrike Falcon Sensor"; wantedBy = [ "multi-user.target" ]; + # Falcon does not stop cleanly during nixos-rebuild switch, leaving + # vendor processes behind and causing restart attempts to fail with + # "Device or resource busy". Keep the running sensor in place during + # switch and apply unit changes on the next boot or manual restart. + restartIfChanged = false; unitConfig.DefaultDependencies = false; + # Start-limit directives belong in [Unit], not [Service]. + unitConfig.StartLimitIntervalSec = 0; after = [ "local-fs.target" ]; conflicts = [ "shutdown.target" ]; before = [ @@ -48,9 +55,6 @@ in Restart = "on-failure"; RestartSec = "15s"; - # Avoid systemd giving up during flapping - StartLimitIntervalSec = 0; - TimeoutStopSec = "60s"; KillMode = "process"; Delegate = true; diff --git a/named-hosts/matic/kolide.nix b/named-hosts/matic/kolide.nix index 4fffda7a7..a498eeeed 100644 --- a/named-hosts/matic/kolide.nix +++ b/named-hosts/matic/kolide.nix @@ -12,9 +12,11 @@ # sudo sh -c 'cat /tmp/kolide-deb/etc/kolide-k2/secret > /etc/kolide-k2/secret' # sudo chown root:root /etc/kolide-k2/secret # sudo chmod 600 /etc/kolide-k2/secret -{ pkgs, ... }: +{ lib, pkgs, ... }: let + falconInstalled = builtins.pathExists /etc/nixos/falcon-sensor.deb; + # Official Kolide NixOS module # Pin to specific commit to avoid hash mismatches when upstream pushes to main. # To update: get latest commit from https://github.com/kolide/nix-agent @@ -37,8 +39,16 @@ in "f /var/lib/dpkg/status 0644 root root - Package: falcon-sensor\\nStatus: install ok installed\\nPriority: optional\\nSection: misc\\nInstalled-Size: 0\\nMaintainer: CrowdStrike\\nArchitecture: amd64\\nVersion: 7.31.0-18410\\nDescription: CrowdStrike Falcon Sensor (shim for Kolide/osquery on NixOS)\\n" ]; - # Add dpkg to Kolide service PATH for deb_packages table - systemd.services.kolide-launcher.path = with pkgs; [ dpkg ]; + systemd.services.kolide-launcher = { + # Kolide shells out to both dpkg-backed package checks and gsettings-based + # screen-lock checks, neither of which are on PATH by default on NixOS. + path = with pkgs; [ dpkg glib ]; + + # When Falcon is installed, start Kolide after it so the initial CrowdStrike + # compliance query does not race the sensor startup during boot/switch. + after = lib.optional falconInstalled "falcon-sensor.service"; + wants = lib.optional falconInstalled "falcon-sensor.service"; + }; # Enable Kolide launcher services.kolide-launcher.enable = true; diff --git a/scripts/find-built-iso.sh b/scripts/find-built-iso.sh new file mode 100644 index 000000000..5b7d412c7 --- /dev/null +++ b/scripts/find-built-iso.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +set -euo pipefail + +result_path="${1:-./result}" + +if [ ! -e "$result_path" ]; then + exit 1 +fi + +case "$result_path" in + *.iso) + if [ -f "$result_path" ]; then + printf '%s\n' "$result_path" + exit 0 + fi + ;; +esac + +resolved_path="" +if command -v readlink >/dev/null 2>&1; then + resolved_path="$(readlink -f "$result_path" 2>/dev/null || true)" +fi +if [ -z "$resolved_path" ] && command -v realpath >/dev/null 2>&1; then + resolved_path="$(realpath "$result_path" 2>/dev/null || true)" +fi + +case "$resolved_path" in + *.iso) + if [ -f "$resolved_path" ]; then + printf '%s\n' "$resolved_path" + exit 0 + fi + ;; +esac + +iso_path="$(find -L "$result_path" -type f -name '*.iso' | sort | head -n 1)" +if [ -n "$iso_path" ]; then + printf '%s\n' "$iso_path" + exit 0 +fi + +exit 1 diff --git a/scripts/llm-update.sh b/scripts/llm-update.sh index 0269b3897..50aa91443 100755 --- a/scripts/llm-update.sh +++ b/scripts/llm-update.sh @@ -12,6 +12,19 @@ MODELS="$ROOT/models.json" exit 1 } +require_command() { + local cmd="$1" + command -v "$cmd" >/dev/null 2>&1 || { + echo "ERROR: required command '$cmd' not found" >&2 + exit 1 + } +} + +require_command jq +require_command sed +require_command mktemp +require_command mv + # jq function: derive display name from a model ID # claude-opus-4.6 → "Claude Opus 4.6", claude-sonnet-4.5-20250929 → "Claude Sonnet 4.5" # shellcheck disable=SC2016 @@ -33,15 +46,28 @@ JQ_PRETTY='def pretty: # Build sed args from models.json # Order: PRETTY, NONDOT, then base — longer patterns must be replaced first sed_args=() +model_rows="$( + jq -r "$JQ_PRETTY"' + to_entries[] | + [.key, .value, (.value | pretty), (.value | gsub("\\.";"-"))] | + @tsv' "$MODELS" +)" || { + echo "ERROR: failed to parse models.json" >&2 + exit 1 +} + +[[ -n "$model_rows" ]] || { + echo "ERROR: no model rows generated from $MODELS" >&2 + exit 1 +} + while IFS=$'\t' read -r key value pretty nondot; do + [[ -n "$key" ]] || continue placeholder="__$(echo "$key" | tr 'a-z-' 'A-Z_')__" sed_args+=(-e "s|${placeholder%__}_PRETTY__|${pretty}|g") sed_args+=(-e "s|${placeholder%__}_NONDOT__|${nondot}|g") sed_args+=(-e "s|${placeholder}|${value}|g") -done < <(jq -r "$JQ_PRETTY"' - to_entries[] | - [.key, .value, (.value | pretty), (.value | gsub("\\.";"-"))] | - @tsv' "$MODELS") +done <<< "$model_rows" # Template → output pairs declare -A TEMPLATES=( @@ -82,7 +108,18 @@ for src in "${!TEMPLATES[@]}"; do echo "SKIP: $src" continue } - sed "${sed_args[@]}" "$ROOT/$src" >"$ROOT/$dst" + + tmp=$(mktemp "$ROOT/.llm-update.XXXXXX") + if ! sed "${sed_args[@]}" "$ROOT/$src" >"$tmp"; then + rm -f "$tmp" + echo "ERROR: sed failed for $src" >&2 + exit 1 + fi + if ! mv -f "$tmp" "$ROOT/$dst"; then + rm -f "$tmp" + echo "ERROR: failed to update $dst" >&2 + exit 1 + fi echo "OK: $dst" done diff --git a/spec/coverage_spec.sh b/spec/coverage_spec.sh index cc795c9d5..e5bfc07a3 100644 --- a/spec/coverage_spec.sh +++ b/spec/coverage_spec.sh @@ -173,6 +173,10 @@ It 'has spec file for scripts/check-nix-inline-scripts.sh' The path "spec/check_nix_inline_scripts_spec.sh" should be exist End +It 'has spec file for scripts/find-built-iso.sh' +The path "spec/find_built_iso_spec.sh" should be exist +End + It 'has spec file for home-manager/services/cliproxyapi/scripts/docker-start.sh' The path "spec/cliproxyapi_docker_start_spec.sh" should be exist End diff --git a/spec/find_built_iso_spec.sh b/spec/find_built_iso_spec.sh new file mode 100644 index 000000000..cb63a05e7 --- /dev/null +++ b/spec/find_built_iso_spec.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2329 + +Describe 'scripts/find-built-iso.sh' +SCRIPT="$PWD/scripts/find-built-iso.sh" + +Describe 'script properties' +It 'uses bash shebang' +When run bash -c "head -1 '$SCRIPT'" +The output should include '#!/usr/bin/env bash' +End + +It 'uses strict mode' +When run bash -c "grep 'set -euo pipefail' '$SCRIPT'" +The output should include 'set -euo pipefail' +End +End + +Describe 'ISO discovery' +setup_iso_fixture() { + local temp_dir="$1" + mkdir -p "$temp_dir/out/iso" + : >"$temp_dir/out/iso/test.iso" +} + +It 'finds an ISO beneath a symlinked result directory' + temp_dir="$(mktemp -d)" + setup_iso_fixture "$temp_dir" + ln -s "$temp_dir/out" "$temp_dir/result" + + When run bash "$SCRIPT" "$temp_dir/result" + The status should be success + The output should equal "$temp_dir/result/iso/test.iso" + + rm -rf "$temp_dir" +End + +It 'returns a direct ISO file path' + temp_dir="$(mktemp -d)" + : >"$temp_dir/direct.iso" + + When run bash "$SCRIPT" "$temp_dir/direct.iso" + The status should be success + The output should equal "$temp_dir/direct.iso" + + rm -rf "$temp_dir" +End + +It 'returns the resolved ISO file for a symlinked result file' + temp_dir="$(mktemp -d)" + : >"$temp_dir/direct.iso" + ln -s "$temp_dir/direct.iso" "$temp_dir/result" + + When run bash "$SCRIPT" "$temp_dir/result" + The status should be success + The output should equal "$temp_dir/direct.iso" + + rm -rf "$temp_dir" +End + +It 'fails when no ISO exists' + temp_dir="$(mktemp -d)" + mkdir -p "$temp_dir/out" + ln -s "$temp_dir/out" "$temp_dir/result" + + When run bash "$SCRIPT" "$temp_dir/result" + The status should not be success + + rm -rf "$temp_dir" +End +End + +End diff --git a/spec/llm_update_spec.sh b/spec/llm_update_spec.sh index 856530e4d..578631d76 100644 --- a/spec/llm_update_spec.sh +++ b/spec/llm_update_spec.sh @@ -26,6 +26,11 @@ It 'exits if models.json is missing' When run bash -c "grep 'models.json not found' '$SCRIPT'" The output should include 'ERROR' End + +It 'checks for jq before generating outputs' +When run bash -c "grep 'require_command jq' '$SCRIPT'" +The output should include 'require_command jq' +End End Describe 'template processing' @@ -69,6 +74,29 @@ The output should include '_pixel_function.tpl.fish' End End +Describe 'generated fish wrapper outputs' +Parameters:dynamic +for file in $(git ls-files 'home-manager/programs/fish/functions/*.tpl.fish'); do + output_file=${file/.tpl/} + %data "$file" "$output_file" +done +End + +It 'has a generated fish wrapper sibling: $2' +The path "$2" should be exist +End + +It 'keeps generated fish wrappers non-empty: $2' +When run bash -c "[ -s '$2' ]" +The status should be success +End + +It 'resolves placeholders in generated fish wrappers: $2' +When run bash -c "! grep -Eq '__[A-Z0-9_]+__' '$2'" +The status should be success +End +End + Describe 'jq pretty-printing' It 'defines a jq pretty function for model names' When run bash -c "grep 'def pretty' '$SCRIPT'" @@ -98,4 +126,48 @@ The output should include '_NONDOT__' End End +Describe 'failure handling' +setup_failure_fixture() { + TEMP_ROOT=$(mktemp -d) + MOCK_BIN=$(mktemp -d) + TARGET="$TEMP_ROOT/home-manager/programs/fish/functions/_pixelh_function.fish" + + mkdir -p "$TEMP_ROOT/scripts" "$(dirname "$TARGET")" + cp -f "$SCRIPT" "$TEMP_ROOT/scripts/llm-update.sh" + cp -f "$PWD/models.json" "$TEMP_ROOT/models.json" + cp -f "$PWD/home-manager/programs/fish/functions/_pixelh_function.tpl.fish" "$TEMP_ROOT/home-manager/programs/fish/functions/_pixelh_function.tpl.fish" + printf 'sentinel\n' >"$TARGET" + + cat >"$MOCK_BIN/jq" <<'EOF' +#!/usr/bin/env bash +echo "mock jq failure" >&2 +exit 127 +EOF + chmod +x "$MOCK_BIN/jq" + + REAL_BIN_DIRS=$( + for cmd in bash dirname mktemp mv sed tr awk paste; do + dirname "$(command -v "$cmd")" + done | awk '!seen[$0]++' | paste -sd: + ) +} + +cleanup_failure_fixture() { + rm -rf "$TEMP_ROOT" "$MOCK_BIN" +} + +Before 'setup_failure_fixture' +After 'cleanup_failure_fixture' + +It 'fails fast when jq is unavailable' +When run bash -c "PATH='$MOCK_BIN:$REAL_BIN_DIRS' bash '$TEMP_ROOT/scripts/llm-update.sh' >/dev/null 2>&1" +The status should not be success +End + +It 'preserves generated outputs when jq is unavailable' +When run bash -c "PATH='$MOCK_BIN:$REAL_BIN_DIRS' bash '$TEMP_ROOT/scripts/llm-update.sh' >/dev/null 2>&1 || true; cat '$TARGET'" +The output should equal 'sentinel' +End +End + End