diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..e37e787f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + # Keep the GitHub Actions used by the workflows pinned and current. + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + labels: + - "dependencies" + - "automated" diff --git a/.github/workflows/fmt.yml b/.github/workflows/fmt.yml index 064060a6..8ccd0778 100644 --- a/.github/workflows/fmt.yml +++ b/.github/workflows/fmt.yml @@ -1,4 +1,4 @@ -name: Nix Format Check +name: Nix Format & Lint Check on: push: @@ -13,7 +13,7 @@ on: jobs: fmt: - name: Check Nix formatting + name: Check Nix formatting & lint runs-on: ubuntu-latest steps: - name: Checkout repository @@ -26,7 +26,11 @@ jobs: extra_nix_config: | experimental-features = nix-command flakes - - name: Check formatting + - name: Cache Nix store + uses: DeterminateSystems/magic-nix-cache-action@v8 + + # Builds the treefmt check (alejandra + statix + deadnix). Fails if any + # file is unformatted or trips a lint. `nix fmt` locally fixes them. + - name: Check formatting & lint working-directory: nixos - run: | - nix fmt -- --check . + run: nix build .#checks.x86_64-linux.treefmt -L diff --git a/.github/workflows/update-flake-lock.yml b/.github/workflows/update-flake-lock.yml new file mode 100644 index 00000000..9d8e2f90 --- /dev/null +++ b/.github/workflows/update-flake-lock.yml @@ -0,0 +1,30 @@ +name: Update flake.lock + +on: + schedule: + # Mondays at 06:00 UTC + - cron: "0 6 * * 1" + workflow_dispatch: {} # allow manual runs + +jobs: + update-lock: + name: Update nixos/flake.lock + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@main + + - name: Update flake.lock and open PR + uses: DeterminateSystems/update-flake-lock@main + with: + path-to-flake-dir: nixos + pr-title: "chore(flake): update flake.lock" + pr-labels: | + dependencies + automated diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 5ac66e51..1d38954e 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -84,3 +84,9 @@ jobs: run: | echo "Building moria..." nix build .#darwinConfigurations.moria.config.system.build.toplevel --dry-run + + - name: Validate citadel + working-directory: nixos + run: | + echo "Building citadel..." + nix build .#darwinConfigurations.citadel.config.system.build.toplevel --dry-run diff --git a/nixos/CLAUDE.md b/nixos/CLAUDE.md index c1aa7438..f2012507 100644 --- a/nixos/CLAUDE.md +++ b/nixos/CLAUDE.md @@ -124,24 +124,55 @@ See [.devcontainer/README.md](.devcontainer/README.md) for details. If needed, see [README.md](README.md) for detailed documentation on repository structure, VM setup, and development workflows. -## Future: Automatic Nix Garbage Collection for Darwin Hosts +## Automatic Nix Garbage Collection for Darwin Hosts -Both darwin hosts use Determinate Nix (`nix.enable = false` in `modules/darwin/common.nix`), -which means nix-darwin's built-in `nix.gc` module **cannot** be used (it asserts `nix.gc.automatic -requires nix.enable`). If store bloat becomes a problem, create a custom launchd daemon in -`modules/darwin/nix-gc.nix`: +The darwin hosts run Determinate Nix (`nix.enable = false` in `modules/darwin/common.nix`), +so nix-darwin's built-in `nix.gc` module can't be used (it asserts `nix.gc.automatic` requires +`nix.enable`). **A hand-rolled `launchd.daemons.nix-gc` is no longer the right answer** — +Determinate Nix ships **Determinate Nixd**, a daemon that performs garbage collection natively. + +To enable scheduled GC, adopt the Determinate nix-darwin module instead of writing custom +launchd jobs: + +1. Add the flake input: `determinate.url = "github:DeterminateSystems/determinate"`. +2. Import `inputs.determinate.darwinModules.default` in `modules/darwin/common.nix`. +3. Configure the collector, e.g.: ```nix -{ ... }: { - launchd.daemons.nix-gc = { - command = "/nix/var/nix/profiles/default/bin/nix-collect-garbage --delete-older-than 30d"; - serviceConfig = { - RunAtLoad = false; - StartCalendarInterval = [{ Weekday = 7; Hour = 3; Minute = 0; }]; - }; - }; +{ + determinate-nix.customSettings = { }; + # GC strategy lives under the Determinate Nixd config: + # /etc/determinate/config.json (written by the module) + # e.g. garbageCollector.strategy = "automatic"; } ``` -Import it from each host's `default.nix`. Runs as root (launchd daemons default), so it -cleans both user and system generations — without `sudo`, only user generations are collected. +See for the current option names +(`determinateNixd.garbageCollector.strategy`). This replaces — and is simpler than — the +old custom-daemon approach, and is correctly disk-pressure aware. + +## Secret Management — Decision Record (1Password vs. agenix / sops-nix) + +**Current approach (keep):** Secrets live in 1Password (vault **Infra**). Committed `.tpl` +files hold `{{ op://Infra/Item/field }}` references; `just secrets` runs `op inject` to +write the real (gitignored) files. See the toolbox root `CLAUDE.md` → "Secret Management" +for the exact commands and prerequisites. + +**Why it's worth a note:** `op inject` writes **plaintext** generated files to disk and +needs an interactive 1Password GUI unlock. On headless **dungeon** that means connecting +via VNC + Touch ID before every `just secrets` — a manual, non-reproducible step that +doesn't fit the otherwise declarative activation flow. + +**Alternatives considered (not adopted this round):** + +- **agenix** — secrets are `age`-encrypted *into the repo* (safe to commit) and decrypted + at activation to a tmpfs (RAM, never written to disk) using each host's existing SSH host + key. Simplest fit for our small set of standalone tokens; no GUI, no manual step, works + headless. Tradeoff: re-keying when host keys change, and editing requires the `agenix` CLI. +- **sops-nix** — same activation-time, key-based decryption but with `sops`/YAML/`age` and + better ergonomics for *bundled* multi-key secret files. More machinery than we need today. + +**Decision: defer.** 1Password stays the source of truth. The manual headless `just secrets` +step is tolerable while dungeon is the only headless Darwin host. **Revisit (lean agenix)** +if a second headless host appears, or if the VNC-unlock dance becomes a recurring pain — +both decrypt at activation to tmpfs and eliminate the plaintext-on-disk + GUI-unlock steps. diff --git a/nixos/README.md b/nixos/README.md index ceee436e..6e38decf 100644 --- a/nixos/README.md +++ b/nixos/README.md @@ -43,17 +43,14 @@ See [.devcontainer/README.md](.devcontainer/README.md) for more details. ### Git Hooks -Git hooks are available to automate validation: +Git hooks are managed declaratively via [git-hooks.nix](https://github.com/cachix/git-hooks.nix) +(see [flake-modules/dev.nix](flake-modules/dev.nix)) and installed automatically when you +enter the dev shell (`nix develop` from `nixos/`): -- **pre-commit**: Runs `nix fmt` on staged `.nix` files -- **pre-push**: Validates all NixOS configs using the devcontainer +- **pre-commit**: Runs `treefmt` (alejandra + statix + deadnix) on staged `.nix` files +- **pre-push**: Runs `nix flake check` -**Installation:** -```bash -./nixos/scripts/install-hooks.sh -``` - -After installation, the hooks run automatically. To skip (not recommended): +To skip (not recommended): ```bash git commit --no-verify git push --no-verify @@ -61,7 +58,10 @@ git push --no-verify ## Pre Configured Shell -We have a pre configured shell available, with all the required tooling. Simply run `$ nix-shell` and you have everything set up. This is powered by our `shell.nix` file, and is useful on a freshly installed NixOS machine. +A reproducible dev shell with all required tooling (treefmt, just, nh, ...) is defined as the +flake's default dev shell in [flake-modules/dev.nix](flake-modules/dev.nix). From `nixos/`, run +`$ nix develop` (or let direnv do it) to get everything set up — useful on a freshly installed +machine. Entering the shell also installs the git hooks above. ## Claude Code Best Practices diff --git a/nixos/flake-modules/dev.nix b/nixos/flake-modules/dev.nix new file mode 100644 index 00000000..29908c64 --- /dev/null +++ b/nixos/flake-modules/dev.nix @@ -0,0 +1,41 @@ +# Dev shell + git hooks. +# +# Replaces the standalone shell.nix and the hand-rolled scripts/hooks/*: +# * `nix develop` gives a reproducible env (treefmt + the tools the justfile uses) +# and installs the git hooks on entry. +# * pre-commit runs treefmt (alejandra + statix + deadnix) on staged files. +# * pre-push runs `nix flake check` so broken configs never reach the remote. +{ + perSystem = { + config, + pkgs, + ... + }: { + pre-commit.settings.hooks = { + treefmt = { + enable = true; + package = config.treefmt.build.wrapper; + }; + + flake-check = { + enable = true; + name = "nix flake check"; + entry = "nix flake check"; + pass_filenames = false; + stages = ["pre-push"]; + }; + }; + + devShells.default = pkgs.mkShell { + inputsFrom = [config.treefmt.build.devShell]; + shellHook = config.pre-commit.installationScript; + packages = with pkgs; [ + git + just + nh + nix-output-monitor + nvd + ]; + }; + }; +} diff --git a/nixos/flake-modules/hosts.nix b/nixos/flake-modules/hosts.nix new file mode 100644 index 00000000..003fbfda --- /dev/null +++ b/nixos/flake-modules/hosts.nix @@ -0,0 +1,110 @@ +# NixOS + nix-darwin host definitions. +# +# Each host used to repeat an identical specialArgs + module list; the mkNixos/ +# mkDarwin helpers below collapse that boilerplate so a host is one entry +# (system + module path + any host-specific extra modules / vars overrides). +{ + inputs, + self, + ... +}: let + inherit (inputs) nixpkgs nixos-wsl nixos-hardware home-manager stylix darwin; + + vars = import ../config/vars.nix {inherit (nixpkgs) lib;}; + + # Minimal home-manager wiring shared by every host. + mkHomeManagerModule = _: { + home-manager = { + useUserPackages = true; + backupFileExtension = "backup"; + users.${vars.user.name} = {}; + }; + }; + + mkNixos = { + system, + modulePath, + extraModules ? [], + hostVars ? vars, + }: + nixpkgs.lib.nixosSystem { + inherit system; + specialArgs = { + inherit inputs; + vars = hostVars; + outputs = self; + }; + modules = + [ + modulePath + stylix.nixosModules.stylix + home-manager.nixosModules.home-manager + mkHomeManagerModule + ] + ++ extraModules; + }; + + mkDarwin = { + modulePath, + system ? "aarch64-darwin", + extraModules ? [], + hostVars ? vars, + }: + darwin.lib.darwinSystem { + inherit system; + specialArgs = { + inherit inputs; + vars = hostVars; + outputs = self; + }; + modules = + [ + modulePath + home-manager.darwinModules.home-manager + ] + ++ extraModules; + }; +in { + flake.nixosConfigurations = { + foundation = mkNixos { + system = "x86_64-linux"; + modulePath = ../hosts/vms/foundation; + extraModules = [nixos-wsl.nixosModules.default]; + }; + + isengard = mkNixos { + system = "x86_64-linux"; + modulePath = ../hosts/pcs/isengard; + extraModules = [nixos-hardware.nixosModules.lenovo-thinkpad-t420]; + }; + + home-lab = mkNixos { + system = "x86_64-linux"; + modulePath = ../hosts/vms/home-lab; + }; + + # Writerdeck — console-only, distraction-free writing machine. + # See: https://veronicaexplains.net/my-first-writerdeck/ + rohan = mkNixos { + system = "x86_64-linux"; + modulePath = ../hosts/pcs/rohan; + }; + + mines = mkNixos { + system = "aarch64-linux"; + modulePath = ../hosts/vms/mines; + }; + }; + + flake.darwinConfigurations = { + dungeon = mkDarwin {modulePath = ../hosts/macs/dungeon;}; + + moria = mkDarwin {modulePath = ../hosts/macs/moria;}; + + # Mozilla work machine: override the user name (drives git identity, home dir). + citadel = mkDarwin { + modulePath = ../hosts/macs/citadel; + hostVars = vars // {user = vars.user // {name = "greghilston";};}; + }; + }; +} diff --git a/nixos/flake-modules/treefmt.nix b/nixos/flake-modules/treefmt.nix new file mode 100644 index 00000000..a024a7ea --- /dev/null +++ b/nixos/flake-modules/treefmt.nix @@ -0,0 +1,22 @@ +# Formatting + linting, exposed as `nix fmt` and a `nix flake check` check. +# +# treefmt-nix wires these together: alejandra formats, statix flags anti-patterns, +# deadnix removes dead code. `nix fmt` applies fixes; `nix flake check` fails on diff. +{ + perSystem = _: { + treefmt = { + projectRootFile = "flake.nix"; + + programs = { + alejandra.enable = true; + statix.enable = true; + deadnix = { + enable = true; + # Leave unused function args (e.g. `{ pkgs, ... }`) alone — they are + # part of NixOS/home-manager module signatures, not dead code. + no-lambda-pattern-names = true; + }; + }; + }; + }; +} diff --git a/nixos/flake.lock b/nixos/flake.lock index 557ceab8..cf7fe2c5 100644 --- a/nixos/flake.lock +++ b/nixos/flake.lock @@ -126,6 +126,22 @@ } }, "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "owner": "NixOS", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_2": { "flake": false, "locked": { "lastModified": 1767039857, @@ -142,6 +158,26 @@ } }, "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1778716662, + "narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_2": { "inputs": { "nixpkgs-lib": [ "nur", @@ -162,7 +198,7 @@ "type": "github" } }, - "flake-parts_2": { + "flake-parts_3": { "inputs": { "nixpkgs-lib": [ "stylix", @@ -219,6 +255,49 @@ "type": "github" } }, + "git-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1781733627, + "narHash": "sha256-U3yTuGBnmXvXoQI3qkpfEDsn9RovQPAjN7ndRco+3u0=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "3bbec39bc90eadfa031e6f3b77272f3f60803e39", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, "gnome-shell": { "flake": false, "locked": { @@ -292,7 +371,7 @@ }, "nixos-wsl": { "inputs": { - "flake-compat": "flake-compat", + "flake-compat": "flake-compat_2", "nixpkgs": "nixpkgs_3" }, "locked": { @@ -392,7 +471,7 @@ }, "nur": { "inputs": { - "flake-parts": "flake-parts", + "flake-parts": "flake-parts_2", "nixpkgs": "nixpkgs_5" }, "locked": { @@ -438,7 +517,9 @@ "inputs": { "claude-desktop": "claude-desktop", "darwin": "darwin", + "flake-parts": "flake-parts", "flake-utils": "flake-utils", + "git-hooks": "git-hooks", "home-manager": "home-manager", "nix-vscode-extensions": "nix-vscode-extensions", "nixos-hardware": "nixos-hardware", @@ -446,7 +527,8 @@ "nixpkgs": "nixpkgs_4", "nur": "nur", "stylix": "stylix", - "systems": "systems_2" + "systems": "systems_2", + "treefmt-nix": "treefmt-nix" } }, "stylix": { @@ -456,7 +538,7 @@ "base16-helix": "base16-helix", "base16-vim": "base16-vim", "firefox-gnome-theme": "firefox-gnome-theme", - "flake-parts": "flake-parts_2", + "flake-parts": "flake-parts_3", "gnome-shell": "gnome-shell", "nixpkgs": [ "nixpkgs" @@ -575,6 +657,26 @@ "repo": "base16-zed", "type": "github" } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1780220602, + "narHash": "sha256-eynAfOmbmxJnkp7YewvCEbShNnnYJ9gLLqkzsYtBPeM=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "db947814a175b7ca6ded66e21383d938df01c227", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } } }, "root": "root", diff --git a/nixos/flake.nix b/nixos/flake.nix index e90de661..fe79e648 100644 --- a/nixos/flake.nix +++ b/nixos/flake.nix @@ -8,6 +8,15 @@ nixos-hardware.url = "github:NixOS/nixos-hardware/master"; # Use the full default set (includes Darwin) systems.url = "github:nix-systems/default"; + + # Flake structure + dev tooling + flake-parts.url = "github:hercules-ci/flake-parts"; + flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs"; + treefmt-nix.url = "github:numtide/treefmt-nix"; + treefmt-nix.inputs.nixpkgs.follows = "nixpkgs"; + git-hooks.url = "github:cachix/git-hooks.nix"; + git-hooks.inputs.nixpkgs.follows = "nixpkgs"; + # Home manager home-manager = { url = "github:nix-community/home-manager"; @@ -25,160 +34,22 @@ # removed from nixpkgs-unstable on 2026-03-03. Let it use its own pinned nixpkgs. claude-desktop.inputs.flake-utils.follows = "flake-utils"; - # nix-darwin for macOS support (Keep input if you might use it later, even if no configs defined now) + # nix-darwin for macOS support darwin.url = "github:LnL7/nix-darwin"; darwin.inputs.nixpkgs.follows = "nixpkgs"; }; - outputs = inputs @ { - self, - nixpkgs, - systems, - nixos-wsl, - nixos-hardware, - home-manager, - stylix, - darwin, # Keep this input if you anticipate defining macOS configs later - ... - }: let - lib = nixpkgs.lib // home-manager.lib; - # Support all major systems (Linux and Darwin) - allSystems = import systems; - forEachSystem = f: lib.genAttrs allSystems (system: f pkgsFor.${system}); - pkgsFor = lib.genAttrs allSystems ( - system: - import nixpkgs { - inherit system; - config = {allowUnfree = true;}; - } - ); - vars = import ./config/vars.nix {inherit (nixpkgs) lib;}; - - # Helper for home-manager module - mkHomeManagerModule = {config, ...}: { - home-manager = { - useUserPackages = true; - backupFileExtension = "backup"; - users.${vars.user.name} = {}; - }; - }; - in { - formatter = forEachSystem (pkgs: pkgs.alejandra); - - nixosConfigurations = { - foundation = nixpkgs.lib.nixosSystem { - system = "x86_64-linux"; - specialArgs = { - inherit inputs vars; - outputs = self; - }; - modules = [ - nixos-wsl.nixosModules.default - ./hosts/vms/foundation - stylix.nixosModules.stylix - home-manager.nixosModules.home-manager - mkHomeManagerModule - ]; - }; - - isengard = nixpkgs.lib.nixosSystem { - system = "x86_64-linux"; - specialArgs = { - inherit inputs vars; - outputs = self; - }; - modules = [ - nixos-hardware.nixosModules.lenovo-thinkpad-t420 - ./hosts/pcs/isengard - stylix.nixosModules.stylix - home-manager.nixosModules.home-manager - mkHomeManagerModule - ]; - }; - - home-lab = nixpkgs.lib.nixosSystem { - system = "x86_64-linux"; - specialArgs = { - inherit inputs vars; - outputs = self; - }; - modules = [ - ./hosts/vms/home-lab - stylix.nixosModules.stylix - home-manager.nixosModules.home-manager - mkHomeManagerModule - ]; - }; - - # Writerdeck — console-only, distraction-free writing machine - # See: https://veronicaexplains.net/my-first-writerdeck/ - rohan = nixpkgs.lib.nixosSystem { - system = "x86_64-linux"; - specialArgs = { - inherit inputs vars; - outputs = self; - }; - modules = [ - ./hosts/pcs/rohan - stylix.nixosModules.stylix - home-manager.nixosModules.home-manager - mkHomeManagerModule - ]; - }; - - # This is the correct configuration for 'mines' as an ARM NixOS VM - mines = nixpkgs.lib.nixosSystem { - system = "aarch64-linux"; # Correct system type - specialArgs = { - inherit inputs vars; - outputs = self; - }; - modules = [ - ./hosts/vms/mines # This points to its specific host config - stylix.nixosModules.stylix - home-manager.nixosModules.home-manager - mkHomeManagerModule - ]; - }; + outputs = inputs: + inputs.flake-parts.lib.mkFlake {inherit inputs;} { + # Systems the per-system outputs (formatter, checks, devShells) are built for. + systems = import inputs.systems; + + imports = [ + inputs.treefmt-nix.flakeModule + inputs.git-hooks.flakeModule + ./flake-modules/hosts.nix + ./flake-modules/treefmt.nix + ./flake-modules/dev.nix + ]; }; - - darwinConfigurations = { - dungeon = darwin.lib.darwinSystem { - system = "aarch64-darwin"; - specialArgs = { - inherit inputs vars; - outputs = self; - }; - modules = [ - ./hosts/macs/dungeon - home-manager.darwinModules.home-manager - ]; - }; - - moria = darwin.lib.darwinSystem { - system = "aarch64-darwin"; - specialArgs = { - inherit inputs vars; - outputs = self; - }; - modules = [ - ./hosts/macs/moria - home-manager.darwinModules.home-manager - ]; - }; - - citadel = darwin.lib.darwinSystem { - system = "aarch64-darwin"; - specialArgs = { - inherit inputs; - vars = vars // {user = vars.user // {name = "greghilston";};}; - outputs = self; - }; - modules = [ - ./hosts/macs/citadel - home-manager.darwinModules.home-manager - ]; - }; - }; - }; } diff --git a/nixos/hosts/macs/citadel/default.nix b/nixos/hosts/macs/citadel/default.nix index 3daf2491..fb9a2292 100644 --- a/nixos/hosts/macs/citadel/default.nix +++ b/nixos/hosts/macs/citadel/default.nix @@ -1,12 +1,12 @@ { vars, - pkgs, lib, ... }: { imports = [ ../../../modules/darwin/common.nix ../../../modules/darwin/home.nix + ../../../modules/darwin/homebrew-base.nix ../../../modules/darwin/omlx.nix ]; @@ -28,32 +28,15 @@ "/Applications/Docker.app" ]; - # --- Homebrew --- + # --- Homebrew (work-machine extras) --- + # Shared baseline (enable, onActivation, common brews/casks, oMLX agent) comes + # from modules/darwin/homebrew-base.nix. Only citadel-specific additions here. homebrew = { - enable = true; - - onActivation = { - autoUpdate = true; - cleanup = "none"; # don't touch apps not declared here - upgrade = true; - }; - taps = [ - "nikitabobko/tap" # AeroSpace "hashicorp/tap" # Terraform - { - name = "jundot/omlx"; - clone_target = "https://github.com/jundot/omlx"; - } ]; brews = [ - "stow" - "tmux" - "just" - "pandoc" - "gh" - # Node via volta (manages node/npm/npx shims in ~/.volta/bin) "volta" @@ -64,87 +47,33 @@ # Python version management "pyenv" "xz" - - # AI / LLM - "jundot/omlx/omlx" - "pi-coding-agent" ]; casks = [ - # Core - "ghostty" - "firefox" "firefox@nightly" "firefox@developer-edition" - "visual-studio-code" - "1password" "magic-wormhole" # Communication - "slack" "zoom" "google-drive" "thunderbird" - # Media - "spotify" - "vlc" - # Dev - "bruno" - "db-browser-for-sqlite" "google-cloud-sdk" - "ngrok" - "docker-desktop" - "dbeaver-community" - - # Drivers - "displaylink" - # Productivity - "obsidian" - "raycast" - "stats" - "jordanbaird-ice" - "aerospace" - - # Productivity + # Networking "mozilla-vpn" ]; }; - # oMLX launchd agent — starts inference server at login - launchd.user.agents.omlx = { - command = "/opt/homebrew/bin/omlx serve"; - serviceConfig = { - RunAtLoad = true; - KeepAlive = true; - StandardOutPath = "/Users/${vars.user.name}/Library/Logs/omlx.log"; - StandardErrorPath = "/Users/${vars.user.name}/Library/Logs/omlx.log"; - }; + # Deploy oMLX with citadel-specific settings (12GB hot cache for M5 Pro 48GB). + # The stow + jq-merge + restart logic lives in modules/darwin/omlx.nix. + services.omlxDeploy = { + enable = true; + cacheSize = "12GB"; }; - # Deploy oMLX with citadel-specific settings (12GB hot cache for M5 Pro 48GB) - system.activationScripts.postActivation.text = lib.mkBefore '' - export PATH="${pkgs.stow}/bin:${pkgs.jq}/bin:$PATH" - TOOLBOX="/Users/${vars.user.name}/Git/toolbox/dot" - - cd "$TOOLBOX" - stow -R --no-folding omlx - - # Merge base settings.json + citadel overlay → ~/.omlx/settings.json - OMLX_SETTINGS="/Users/${vars.user.name}/.omlx/settings.json" - jq -s '.[0] * .[1]' \ - "$TOOLBOX/omlx/.omlx/settings.json" \ - "$TOOLBOX/omlx-citadel/.omlx/settings.json" \ - > "$OMLX_SETTINGS.tmp" - mv -f "$OMLX_SETTINGS.tmp" "$OMLX_SETTINGS" - - launchctl kickstart -k "gui/$(id -u ${vars.user.name})/org.nixos.omlx" 2>/dev/null || true - - echo "✓ oMLX configured for citadel (hot_cache_max_size=12GB)" - ''; - home-manager.users.${vars.user.name} = { # 6-bit is the best quality/memory balance for 48GB custom.programs.pi.defaultModel = lib.mkForce "Qwen3.6-35B-A3B-6bit"; diff --git a/nixos/hosts/macs/dungeon/default.nix b/nixos/hosts/macs/dungeon/default.nix index 9a9eebf6..e4df7488 100644 --- a/nixos/hosts/macs/dungeon/default.nix +++ b/nixos/hosts/macs/dungeon/default.nix @@ -3,7 +3,60 @@ lib, pkgs, ... -}: { +}: let + # NFS mounts on macOS: nix-darwin has no `fileSystems` support, so each share is a + # launchd daemon that waits for the server to answer ping, then mounts NFSv3. + # KeepAlive(SuccessfulExit=false) + the "already mounted? exit 0" guard means it + # remounts after a network drop without thrashing. This helper holds the shared + # retry/ping/mount logic; only the per-share fields below differ. + mkNfsMountDaemon = { + mountPoint, + server, + path, + retries, + logFile, + }: { + script = '' + MOUNT_POINT="${mountPoint}" + NFS_SERVER="${server}" + NFS_PATH="${path}" + + # Create mount point if it doesn't exist + /bin/mkdir -p "$MOUNT_POINT" + + # If already mounted, nothing to do + if /sbin/mount | /usr/bin/grep -q "$MOUNT_POINT"; then + exit 0 + fi + + # Wait for the NFS server to be reachable (${toString retries} tries × 5s) + for i in $(seq 1 ${toString retries}); do + if /sbin/ping -c 1 -W 1 "$NFS_SERVER" >/dev/null 2>&1; then + break + fi + /bin/sleep 5 + done + + # Mount the NFS share. Flags (macOS NFSv3): + # resvport privileged source port (required on macOS) + # vers=3 macOS defaults to v4, which hangs against these servers + # nolock servers don't run rpc.statd; consistency handled elsewhere + # soft,intr return/interrupt on timeout instead of hanging indefinitely + # rw read-write (Docker bind mounts / backup writes) + /sbin/mount -t nfs -o resvport,vers=3,nolock,soft,intr,rw "$NFS_SERVER:$NFS_PATH" "$MOUNT_POINT" + ''; + serviceConfig = { + RunAtLoad = true; + # Retry every 30s if the mount fails (e.g., server not yet up after reboot) + KeepAlive = { + SuccessfulExit = false; + }; + ThrottleInterval = 30; + StandardOutPath = logFile; + StandardErrorPath = logFile; + }; + }; +in { imports = [ ../../../modules/darwin/common.nix ../../../modules/darwin/homebrew.nix @@ -75,99 +128,25 @@ }; }; - # NFS mount for Unraid data share - # Mirrors the NixOS VM mount at nixos/hosts/vms/home-lab/default.nix (192.168.1.2:/mnt/user/data) - # Mount point matches SERVER_DATA_SHARE_MOUNT_POINT in the home-lab .env - # macOS doesn't support fileSystems in nix-darwin, so we use a launchd daemon instead. - # The daemon waits for the NFS server to be reachable, then mounts. - # KeepAlive + exit-on-already-mounted ensures it remounts after network recovery. - launchd.daemons.mount-unraid-data = { - script = '' - MOUNT_POINT="/Volumes/unraid-data" - NFS_SERVER="${vars.networking.hosts.unraid.lan}" - NFS_PATH="/mnt/user/data" - - # Create mount point if it doesn't exist - /bin/mkdir -p "$MOUNT_POINT" - - # If already mounted, nothing to do - if /sbin/mount | /usr/bin/grep -q "$MOUNT_POINT"; then - exit 0 - fi - - # Wait for NFS server to be reachable (up to 60s) - for i in $(seq 1 12); do - if /sbin/ping -c 1 -W 1 "$NFS_SERVER" >/dev/null 2>&1; then - break - fi - /bin/sleep 5 - done - - # Mount the NFS share - # -o resvport: required on macOS for NFS (uses privileged source port) - # -o vers=3: Unraid exports NFSv3 — macOS defaults to v4 which hangs - # -o nolock: skip NFS locking — Unraid uses local_lock=none, and rpc.statd isn't available - # -o soft: return errors on timeout rather than hanging indefinitely - # -o intr: allow signals to interrupt hung operations - # -o rw: read-write access for Docker container bind mounts - /sbin/mount -t nfs -o resvport,vers=3,nolock,soft,intr,rw "$NFS_SERVER:$NFS_PATH" "$MOUNT_POINT" - ''; - serviceConfig = { - RunAtLoad = true; - # Retry every 30s if the mount fails (e.g., server not yet up after reboot) - KeepAlive = { - SuccessfulExit = false; - }; - # Don't restart too aggressively - ThrottleInterval = 30; - StandardOutPath = "/var/log/mount-unraid-data.log"; - StandardErrorPath = "/var/log/mount-unraid-data.log"; - }; + # NFS mount for Unraid data share (NFSv3 over LAN). Used by Docker container bind + # mounts; mirrors the NixOS VM mount at hosts/vms/home-lab/default.nix and matches + # SERVER_DATA_SHARE_MOUNT_POINT in the home-lab .env. + launchd.daemons.mount-unraid-data = mkNfsMountDaemon { + mountPoint = "/Volumes/unraid-data"; + server = vars.networking.hosts.unraid.lan; + path = "/mnt/user/data"; + retries = 12; # ~60s — LAN server, usually up quickly + logFile = "/var/log/mount-unraid-data.log"; }; - # NFS mount for Fob offsite backup (Raspberry Pi via Tailscale) - # Used by Kopia for offsite backups — not used by Docker containers. - # Tailscale may not be connected at boot, so we retry more aggressively. - launchd.daemons.mount-fob-backup = { - script = '' - MOUNT_POINT="/Volumes/fob-backup" - NFS_SERVER="${vars.networking.hosts.fob.tailscale}" - NFS_PATH="/mnt/mothership" - - # Create mount point if it doesn't exist - /bin/mkdir -p "$MOUNT_POINT" - - # If already mounted, nothing to do - if /sbin/mount | /usr/bin/grep -q "$MOUNT_POINT"; then - exit 0 - fi - - # Wait for NFS server to be reachable (up to 120s — Tailscale may take time) - for i in $(seq 1 24); do - if /sbin/ping -c 1 -W 1 "$NFS_SERVER" >/dev/null 2>&1; then - break - fi - /bin/sleep 5 - done - - # Mount the NFS share - # -o resvport: required on macOS for NFS (uses privileged source port) - # -o vers=3: use NFSv3 explicitly — macOS defaults to v4 which can hang - # -o nolock: skip NFS locking — Fob doesn't run rpc.statd, and Kopia handles its own consistency - # -o soft: return errors on timeout rather than hanging indefinitely - # -o intr: allow signals to interrupt hung operations - # -o rw: read-write access for backup writes - /sbin/mount -t nfs -o resvport,vers=3,nolock,soft,intr,rw "$NFS_SERVER:$NFS_PATH" "$MOUNT_POINT" - ''; - serviceConfig = { - RunAtLoad = true; - KeepAlive = { - SuccessfulExit = false; - }; - ThrottleInterval = 30; - StandardOutPath = "/var/log/mount-fob-backup.log"; - StandardErrorPath = "/var/log/mount-fob-backup.log"; - }; + # NFS mount for Fob offsite backup (Raspberry Pi over Tailscale). Used by Kopia for + # offsite backups, not by Docker. Tailscale may be slow to connect at boot, so wait longer. + launchd.daemons.mount-fob-backup = mkNfsMountDaemon { + mountPoint = "/Volumes/fob-backup"; + server = vars.networking.hosts.fob.tailscale; + path = "/mnt/mothership"; + retries = 24; # ~120s — Tailscale may take time to come up + logFile = "/var/log/mount-fob-backup.log"; }; # Healthchecks.io ping — signals that dungeon is alive and has network. @@ -203,41 +182,25 @@ }; }; - # Deploy oMLX with dungeon-specific settings (8GB hot cache for M3 Pro 36GB) - # See ~/Git/toolbox/dot/omlx/README.md for stow strategy explanation - # Combined with home-lab-config deployment and power management + # Deploy oMLX with dungeon-specific settings (8GB hot cache for M3 Pro 36GB). + # The stow + jq-merge + restart logic lives in modules/darwin/omlx.nix. + services.omlxDeploy = { + enable = true; + cacheSize = "8GB"; + }; + + # Dungeon-specific activation: ser2net dotfiles, clamshell-sleep prevention, + # NFS mount points, and the home-lab repo clone/pull. # NOTE: Uses postActivation (not custom names) because nix-darwin only runs well-known activation script names. system.activationScripts.postActivation.text = '' set -euo pipefail # Exit on error, undefined vars, and pipeline failures - # Deploy oMLX dotfiles: base config + dungeon-specific overrides - # settings.json is excluded from stow (via .stow-local-ignore) because it - # contains host-specific cache sizes AND auth keys. Merged with jq instead. - export PATH="${pkgs.stow}/bin:${pkgs.jq}/bin:$PATH" + # Stow ser2net dotfiles (USB serial exposure for OrbStack containers). + export PATH="${pkgs.stow}/bin:$PATH" TOOLBOX="/Users/${vars.user.name}/Git/toolbox/dot" - # --no-folding prevents stow from symlinking the .omlx/ directory itself - # into the repo. Without it, oMLX writes land directly in the git tree. cd "$TOOLBOX" - stow -R --no-folding omlx stow -R --no-folding ser2net - # Merge base settings.json + dungeon cache overlay → ~/.omlx/settings.json - # Write to a temp file first, then mv into place. This avoids truncating the - # source if ~/.omlx/settings.json is a stale symlink pointing back to it, - # and is atomic (the old file survives if jq fails). - OMLX_SETTINGS="/Users/${vars.user.name}/.omlx/settings.json" - jq -s '.[0] * .[1]' \ - "$TOOLBOX/omlx/.omlx/settings.json" \ - "$TOOLBOX/omlx-dungeon/.omlx/settings.json" \ - > "$OMLX_SETTINGS.tmp" - mv -f "$OMLX_SETTINGS.tmp" "$OMLX_SETTINGS" - - # Restart oMLX so it picks up the merged settings.json. - # KeepAlive only restarts on crashes, not config changes. - launchctl kickstart -k "gui/$(id -u ${vars.user.name})/org.nixos.omlx" 2>/dev/null || true - - echo "✓ oMLX configured for dungeon (hot_cache_max_size=8GB)" - # Prevent clamshell sleep on Apple Silicon (lid-close with no external display). # See the detailed explanation in the power.sleep section above. # diff --git a/nixos/hosts/macs/moria/default.nix b/nixos/hosts/macs/moria/default.nix index f5fca2b5..106c1d90 100644 --- a/nixos/hosts/macs/moria/default.nix +++ b/nixos/hosts/macs/moria/default.nix @@ -24,38 +24,10 @@ ]; }; - # Deploy oMLX with moria-specific settings (32GB hot cache for M4 Max 128GB) - # See ~/Git/toolbox/dot/omlx/README.md for stow strategy explanation - # - # settings.json is excluded from stow (via .stow-local-ignore) because it - # contains host-specific cache sizes AND auth keys that shouldn't be duplicated - # into per-host stow packages. Instead we merge base + overlay with jq. - system.activationScripts.postActivation.text = lib.mkBefore '' - export PATH="${pkgs.stow}/bin:${pkgs.jq}/bin:$PATH" - TOOLBOX="/Users/${vars.user.name}/Git/toolbox/dot" - - # Stow shared oMLX files (model_settings.json, stats.json, etc.) - # --no-folding prevents stow from symlinking the .omlx/ directory itself - # into the repo. Without it, oMLX writes (settings saves, cache, logs) - # land directly in the git working tree. - cd "$TOOLBOX" - stow -R --no-folding omlx - - # Merge base settings.json + moria cache overlay → ~/.omlx/settings.json - # Write to a temp file first, then mv into place. This avoids truncating the - # source if ~/.omlx/settings.json is a stale symlink pointing back to it, - # and is atomic (the old file survives if jq fails). - OMLX_SETTINGS="/Users/${vars.user.name}/.omlx/settings.json" - jq -s '.[0] * .[1]' \ - "$TOOLBOX/omlx/.omlx/settings.json" \ - "$TOOLBOX/omlx-moria/.omlx/settings.json" \ - > "$OMLX_SETTINGS.tmp" - mv -f "$OMLX_SETTINGS.tmp" "$OMLX_SETTINGS" - - # Restart oMLX so it picks up the merged settings.json. - # KeepAlive only restarts on crashes, not config changes. - launchctl kickstart -k "gui/$(id -u ${vars.user.name})/org.nixos.omlx" 2>/dev/null || true - - echo "✓ oMLX configured for moria (hot_cache_max_size=32GB)" - ''; + # Deploy oMLX with moria-specific settings (32GB hot cache for M4 Max 128GB). + # The stow + jq-merge + restart logic lives in modules/darwin/omlx.nix. + services.omlxDeploy = { + enable = true; + cacheSize = "32GB"; + }; } diff --git a/nixos/modules/common/default.nix b/nixos/modules/common/default.nix index 4219900b..42780b1c 100644 --- a/nixos/modules/common/default.nix +++ b/nixos/modules/common/default.nix @@ -6,9 +6,7 @@ pkgs, vars, ... -} @ args: -# Optional: give a name to the whole argument set -{ +}: { imports = [ inputs.home-manager.nixosModules.home-manager ../../modules/stylix diff --git a/nixos/modules/darwin/homebrew-base.nix b/nixos/modules/darwin/homebrew-base.nix new file mode 100644 index 00000000..86e73c5a --- /dev/null +++ b/nixos/modules/darwin/homebrew-base.nix @@ -0,0 +1,91 @@ +# Shared Homebrew baseline for all Darwin hosts. +# +# homebrew.brews/casks/taps are list options, so each host imports this module +# and *adds* its own host-specific extras on top (the module system concatenates +# the lists). Only this module sets the non-list options (enable, onActivation) +# and the oMLX launchd agent, so hosts never need to repeat them. +{vars, ...}: { + # Launch omlx LLM inference server at login so pi and other tools can connect. + # Runs as a user agent (not a system daemon) so it has access to ~/models and ~/.omlx/. + launchd.user.agents.omlx = { + command = "/opt/homebrew/bin/omlx serve"; + serviceConfig = { + RunAtLoad = true; + KeepAlive = true; + StandardOutPath = "/Users/${vars.user.name}/Library/Logs/omlx.log"; + StandardErrorPath = "/Users/${vars.user.name}/Library/Logs/omlx.log"; + }; + }; + + homebrew = { + enable = true; + + onActivation = { + autoUpdate = true; + # "none" — don't touch packages not declared in the config. Change to + # "check" once manually-installed brew drift has been cleaned up. + cleanup = "none"; + upgrade = true; + }; + + taps = [ + "nikitabobko/tap" # AeroSpace + { + name = "jundot/omlx"; + clone_target = "https://github.com/jundot/omlx"; + } # omlx LLM inference server + # GOTCHA: newer Homebrew enforces a tap-trust check that silently aborts the + # omlx source build (it builds from source via rust) with NO output — leaving the + # host stuck on an old version (this is how dungeon got pinned at 0.3.8 while moria + # was on 0.4.4rc1). If `brew upgrade jundot/omlx/omlx` fails with a bare + # "build.rb exited with 1", run `brew trust jundot/omlx` once, or upgrade with + # HOMEBREW_NO_REQUIRE_TAP_TRUST=1. Not a Command Line Tools / OS-version problem. + ]; + + # Common CLI tools — managed via brew rather than nix for easier macOS updates. + brews = [ + "stow" + "tmux" + "just" + "pandoc" + "gh" + + # AI / LLM + "jundot/omlx/omlx" + "pi-coding-agent" + ]; + + # Apps every Darwin host gets. Host-specific apps live in each host's config. + casks = [ + # Core + "ghostty" + "firefox" + "visual-studio-code" + "1password" + + # Communication + "slack" + + # Media + "spotify" + "vlc" + + # Dev + "bruno" + "docker-desktop" + "dbeaver-community" + "db-browser-for-sqlite" + "ngrok" + + # Drivers + "displaylink" + + # Productivity + "obsidian" + "raycast" + "stats" + "jordanbaird-ice" + "aerospace" + ]; + }; +} diff --git a/nixos/modules/darwin/homebrew.nix b/nixos/modules/darwin/homebrew.nix index 305232df..a9f29807 100644 --- a/nixos/modules/darwin/homebrew.nix +++ b/nixos/modules/darwin/homebrew.nix @@ -1,60 +1,18 @@ -{vars, ...}: { - # Launch omlx LLM inference server at login so pi and other tools can connect. - # Runs as a user agent (not a system daemon) so it has access to ~/models and ~/.omlx/. - launchd.user.agents.omlx = { - command = "/opt/homebrew/bin/omlx serve"; - serviceConfig = { - RunAtLoad = true; - KeepAlive = true; - StandardOutPath = "/Users/${vars.user.name}/Library/Logs/omlx.log"; - StandardErrorPath = "/Users/${vars.user.name}/Library/Logs/omlx.log"; - }; - }; +# Homebrew for the "media/server" Darwin hosts (dungeon, moria). +# Imports the shared baseline and adds host-class-specific extras. The oMLX +# launchd agent and the common brews/casks/taps live in homebrew-base.nix. +{...}: { + imports = [./homebrew-base.nix]; homebrew = { - enable = true; - - onActivation = { - autoUpdate = true; - # "none" — don't touch packages not in this file. Change to "check" once - # manually-installed brew drift has been cleaned up on this machine. - cleanup = "none"; - upgrade = true; - }; - - taps = [ - "nikitabobko/tap" # AeroSpace - { - name = "jundot/omlx"; - clone_target = "https://github.com/jundot/omlx"; - } # omlx LLM inference server - # GOTCHA: newer Homebrew enforces a tap-trust check that silently aborts the - # omlx source build (it builds from source via rust) with NO output — leaving the - # host stuck on an old version (this is how dungeon got pinned at 0.3.8 while moria - # was on 0.4.4rc1). If `brew upgrade jundot/omlx/omlx` fails with a bare - # "build.rb exited with 1", run `brew trust jundot/omlx` once, or upgrade with - # HOMEBREW_NO_REQUIRE_TAP_TRUST=1. Not a Command Line Tools / OS-version problem. - ]; - brews = [ - "stow" - "ser2net" # Exposes USB serial devices over TCP for OrbStack containers - - # CLI tools — managed here rather than nix for easier updates on macOS - "tmux" - "just" - "pandoc" - "gh" "go" "hugo" + "ser2net" # Exposes USB serial devices over TCP for OrbStack containers # Runtime (needed by pi for npm: packages) "node" - # AI / LLM - "jundot/omlx/omlx" - "pi-coding-agent" - # Monitoring exporters — scraped by the home-lab Prometheus over # host.docker.internal. Native (not containers) so they report the real Mac, # not OrbStack's Linux VM. See launchd.user.agents in hosts/macs/dungeon. @@ -64,51 +22,20 @@ ]; casks = [ - # Core - "ghostty" - "firefox" "google-chrome" - "visual-studio-code" - - # Communication "discord" - "slack" - - # Media - "spotify" - "vlc" "calibre" - - # Dev - "bruno" - "docker-desktop" "orbstack" - "dbeaver-community" - "db-browser-for-sqlite" - "ngrok" - - # Productivity - "obsidian" - "raycast" "shortcat" - "stats" - "jordanbaird-ice" - "aerospace" + "tailscale-app" + "1password-cli" # AI "claude" "lm-studio" "draw-things" - # Networking - "tailscale-app" - - # Drivers - "displaylink" - # Other - "1password" - "1password-cli" "flux-app" "steam" "godot" diff --git a/nixos/modules/darwin/omlx.nix b/nixos/modules/darwin/omlx.nix index a4037b68..1a93a3ed 100644 --- a/nixos/modules/darwin/omlx.nix +++ b/nixos/modules/darwin/omlx.nix @@ -1,26 +1,90 @@ { + config, lib, + pkgs, vars, ... -}: { - # Activation script for oMLX model setup on Darwin hosts - # Creates model variant symlinks (e.g., Qwen3.6-35B-A3B-8bit-long-context → Qwen3.6-35B-A3B-8bit) - # Allows model_settings.json to define separate configurations without duplicating model weights. - # See: https://github.com/jundot/omlx/issues/341#issuecomment-4202459307 +}: let + cfg = config.services.omlxDeploy; + user = vars.user.name; + host = config.networking.hostName; +in { + options.services.omlxDeploy = { + enable = lib.mkEnableOption '' + oMLX dotfile deployment on this Darwin host: stow the shared oMLX package, + merge the base settings.json with the per-host omlx- overlay via jq, + and restart the launchd agent so it picks up the new settings + ''; - system.activationScripts.postActivation.text = lib.mkAfter '' - # Create oMLX model variant symlinks for multi-configuration support. - # Models are stored in ~/Git/toolbox/dot/omlx/.omlx/models (per .stow-local-ignore) - # and accessed directly by oMLX via settings.json model_dir configuration. - # See: https://github.com/jundot/omlx/issues/341#issuecomment-4202459307 - MODELS_DIR="/Users/${vars.user.name}/Git/toolbox/dot/omlx/.omlx/models" - if [ -d "$MODELS_DIR" ]; then - # Qwen3.6-35B-A3B-8bit-long-context: extended context (1M tokens) for long-form analysis - # Only create if source exists and target doesn't - if [ -d "$MODELS_DIR/Qwen3.6-35B-A3B-8bit" ] && [ ! -e "$MODELS_DIR/Qwen3.6-35B-A3B-8bit-long-context" ]; then - ln -s Qwen3.6-35B-A3B-8bit "$MODELS_DIR/Qwen3.6-35B-A3B-8bit-long-context" - echo "✓ Created model variant symlink: Qwen3.6-35B-A3B-8bit-long-context" - fi - fi - ''; + cacheSize = lib.mkOption { + type = lib.types.str; + example = "8GB"; + description = '' + Human-readable hot-cache size used only for the activation log line. + The real value lives in the host's omlx-/.omlx/settings.json overlay. + ''; + }; + }; + + config = { + # Activation scripts for oMLX on Darwin hosts. + # + # nix-darwin concatenates every postActivation.text fragment into ONE bash + # script, so ordering is controlled with mkBefore/mkAfter: + # * mkBefore — deploy settings.json early, before host-specific blocks + # (pmset, NFS, repo clones) that may depend on a running oMLX. + # * mkAfter — create model-variant symlinks last, after deploy. + system.activationScripts.postActivation.text = lib.mkMerge [ + # ── Deploy: stow + jq settings merge + agent restart (shared across hosts) + (lib.mkIf cfg.enable (lib.mkBefore '' + # Deploy oMLX dotfiles: base config + ${host}-specific overrides. + # settings.json is excluded from stow (via .stow-local-ignore) because it + # contains host-specific cache sizes AND auth keys; we merge with jq instead. + # See ~/Git/toolbox/dot/omlx/README.md for the stow strategy explanation. + export PATH="${pkgs.stow}/bin:${pkgs.jq}/bin:$PATH" + TOOLBOX="/Users/${user}/Git/toolbox/dot" + + # --no-folding prevents stow from symlinking the .omlx/ directory itself + # into the repo. Without it, oMLX writes (settings saves, cache, logs) + # land directly in the git working tree. + cd "$TOOLBOX" + stow -R --no-folding omlx + + # Merge base settings.json + ${host} cache overlay → ~/.omlx/settings.json. + # Write to a temp file first, then mv into place: avoids truncating the + # source if ~/.omlx/settings.json is a stale symlink pointing back to it, + # and is atomic (the old file survives if jq fails). + OMLX_SETTINGS="/Users/${user}/.omlx/settings.json" + jq -s '.[0] * .[1]' \ + "$TOOLBOX/omlx/.omlx/settings.json" \ + "$TOOLBOX/omlx-${host}/.omlx/settings.json" \ + > "$OMLX_SETTINGS.tmp" + mv -f "$OMLX_SETTINGS.tmp" "$OMLX_SETTINGS" + + # Restart oMLX so it picks up the merged settings.json. + # KeepAlive only restarts on crashes, not config changes. + launchctl kickstart -k "gui/$(id -u ${user})/org.nixos.omlx" 2>/dev/null || true + + echo "✓ oMLX configured for ${host} (hot_cache_max_size=${cfg.cacheSize})" + '')) + + # ── Model-variant symlinks for multi-configuration support. + # Creates e.g. Qwen3.6-35B-A3B-8bit-long-context → Qwen3.6-35B-A3B-8bit so + # model_settings.json can define separate configs without duplicating weights. + # See: https://github.com/jundot/omlx/issues/341#issuecomment-4202459307 + (lib.mkAfter '' + # Models live in ~/Git/toolbox/dot/omlx/.omlx/models (per .stow-local-ignore) + # and are accessed directly by oMLX via settings.json model_dir. + MODELS_DIR="/Users/${user}/Git/toolbox/dot/omlx/.omlx/models" + if [ -d "$MODELS_DIR" ]; then + # Qwen3.6-35B-A3B-8bit-long-context: extended context (1M tokens). + # Only create if source exists and target doesn't. + if [ -d "$MODELS_DIR/Qwen3.6-35B-A3B-8bit" ] && [ ! -e "$MODELS_DIR/Qwen3.6-35B-A3B-8bit-long-context" ]; then + ln -s Qwen3.6-35B-A3B-8bit "$MODELS_DIR/Qwen3.6-35B-A3B-8bit-long-context" + echo "✓ Created model variant symlink: Qwen3.6-35B-A3B-8bit-long-context" + fi + fi + '') + ]; + }; } diff --git a/nixos/modules/programs/gui/alacritty/default.nix b/nixos/modules/programs/gui/alacritty/default.nix index 06d6917c..3889cf9b 100644 --- a/nixos/modules/programs/gui/alacritty/default.nix +++ b/nixos/modules/programs/gui/alacritty/default.nix @@ -1,4 +1,4 @@ -{...}: { +_: { programs.alacritty = { enable = true; settings.selection.save_to_clipboard = true; diff --git a/nixos/modules/programs/tui/atuin/default.nix b/nixos/modules/programs/tui/atuin/default.nix index b2e37ac9..52061f99 100644 --- a/nixos/modules/programs/tui/atuin/default.nix +++ b/nixos/modules/programs/tui/atuin/default.nix @@ -1,5 +1,5 @@ # nixos/modules/programs/tui/atuin/default.nix -{...}: { +_: { programs.atuin = { enable = true; enableZshIntegration = true; diff --git a/nixos/modules/programs/tui/eza/default.nix b/nixos/modules/programs/tui/eza/default.nix index 089cf214..490e22ab 100644 --- a/nixos/modules/programs/tui/eza/default.nix +++ b/nixos/modules/programs/tui/eza/default.nix @@ -1,5 +1,5 @@ # nixos/modules/programs/tui/eza/default.nix -{...}: { +_: { programs.eza = { enable = true; enableZshIntegration = true; diff --git a/nixos/modules/programs/tui/fzf/default.nix b/nixos/modules/programs/tui/fzf/default.nix index 07aa1f3f..e863c405 100644 --- a/nixos/modules/programs/tui/fzf/default.nix +++ b/nixos/modules/programs/tui/fzf/default.nix @@ -1,5 +1,5 @@ # nixos/modules/programs/tui/fzf/default.nix -{...}: { +_: { programs.fzf = { enable = true; enableZshIntegration = true; diff --git a/nixos/modules/programs/tui/git/default.nix b/nixos/modules/programs/tui/git/default.nix index 2814c774..fef509b9 100644 --- a/nixos/modules/programs/tui/git/default.nix +++ b/nixos/modules/programs/tui/git/default.nix @@ -26,7 +26,7 @@ settings = { user = { name = vars.user.fullName; - email = vars.user.email; + inherit (vars.user) email; }; # Use difftastic for all diffs diff --git a/nixos/modules/programs/tui/opencode.nix b/nixos/modules/programs/tui/opencode.nix index d5abf7d3..84dde855 100644 --- a/nixos/modules/programs/tui/opencode.nix +++ b/nixos/modules/programs/tui/opencode.nix @@ -38,7 +38,7 @@ in { name = "oMLX (local)"; options = { baseURL = cfg.omlxBaseUrl; - apiKey = cfg.apiKey; + inherit (cfg) apiKey; }; models = { "Qwen3.6-35B-A3B-8bit" = { diff --git a/nixos/modules/programs/tui/pi.nix b/nixos/modules/programs/tui/pi.nix index cd5412c1..711c4709 100644 --- a/nixos/modules/programs/tui/pi.nix +++ b/nixos/modules/programs/tui/pi.nix @@ -45,9 +45,9 @@ in { home.file.".pi/agent/settings.json" = { text = builtins.toJSON { defaultProvider = "omlx"; - defaultModel = cfg.defaultModel; + inherit (cfg) defaultModel; lastChangelogVersion = "0.67.6"; - packages = cfg.packages; + inherit (cfg) packages; }; }; diff --git a/nixos/modules/programs/tui/zellij/default.nix b/nixos/modules/programs/tui/zellij/default.nix index 728fd401..d4ee6fc2 100644 --- a/nixos/modules/programs/tui/zellij/default.nix +++ b/nixos/modules/programs/tui/zellij/default.nix @@ -1,5 +1,5 @@ # nixos/modules/programs/tui/zellij/default.nix -{...}: { +_: { programs.zellij = { enable = true; enableZshIntegration = false; # Don't auto-attach; use manually alongside tmux diff --git a/nixos/modules/programs/tui/zoxide/default.nix b/nixos/modules/programs/tui/zoxide/default.nix index 4f3cc4ce..230a1978 100644 --- a/nixos/modules/programs/tui/zoxide/default.nix +++ b/nixos/modules/programs/tui/zoxide/default.nix @@ -1,5 +1,5 @@ # nixos/modules/programs/tui/zoxide/default.nix -{...}: { +_: { programs.zoxide = { enable = true; enableZshIntegration = true; diff --git a/nixos/scripts/hooks/pre-commit b/nixos/scripts/hooks/pre-commit deleted file mode 100755 index 9d4214c6..00000000 --- a/nixos/scripts/hooks/pre-commit +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env bash -# Pre-commit hook: Format Nix files -# Install with: ./nixos/scripts/install-hooks.sh - -set -e - -# Only run if we have staged nix files in the nixos directory -STAGED_NIX_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^nixos/.*\.nix$' || true) - -if [ -z "$STAGED_NIX_FILES" ]; then - exit 0 -fi - -echo "Running nix fmt on staged files..." - -# Change to nixos directory for formatting -cd "$(git rev-parse --show-toplevel)/nixos" - -# Check if nix is available -if ! command -v nix &> /dev/null; then - echo "Warning: nix not found, skipping format check" - exit 0 -fi - -# Run formatter and check for changes -nix fmt . 2>/dev/null - -# Check if any staged files were modified by the formatter -REFORMATTED=false -for file in $STAGED_NIX_FILES; do - # Get the file path relative to repo root - FULL_PATH="$(git rev-parse --show-toplevel)/$file" - if [ -f "$FULL_PATH" ]; then - # Check if file has unstaged changes (meaning formatter modified it) - if ! git diff --quiet "$FULL_PATH" 2>/dev/null; then - echo "Reformatted: $file" - REFORMATTED=true - fi - fi -done - -if [ "$REFORMATTED" = true ]; then - echo "" - echo "Some files were reformatted. Please review and re-stage the changes:" - echo " git add -p nixos/" - echo " git commit" - exit 1 -fi - -echo "All Nix files formatted correctly." diff --git a/nixos/scripts/hooks/pre-push b/nixos/scripts/hooks/pre-push deleted file mode 100755 index 6513d066..00000000 --- a/nixos/scripts/hooks/pre-push +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env bash -# Pre-push hook: Validate all NixOS configurations using the dev container -# Install with: ./nixos/scripts/install-hooks.sh - -set -e - -REPO_ROOT="$(git rev-parse --show-toplevel)" -NIXOS_DIR="$REPO_ROOT/nixos" - -# Check if Docker is available -if ! command -v docker &> /dev/null; then - echo "Warning: Docker not found, skipping NixOS validation" - echo "Install Docker to enable pre-push validation" - exit 0 -fi - -# Check if Docker daemon is running -if ! docker info &> /dev/null 2>&1; then - echo "Warning: Docker daemon not running, skipping NixOS validation" - exit 0 -fi - -echo "==========================================" -echo "Pre-push: Validating NixOS configurations" -echo "==========================================" - -# Check if the devcontainer image exists, build if not -IMAGE_NAME="nixos-devcontainer" -if ! docker image inspect "$IMAGE_NAME" &> /dev/null; then - echo "Building devcontainer image (first time only)..." - docker build -t "$IMAGE_NAME" "$NIXOS_DIR/.devcontainer" || { - echo "Warning: Failed to build devcontainer image, skipping validation" - exit 0 - } -fi - -# Run validation in the container -echo "Running validation..." -if docker run --rm -v "$NIXOS_DIR:/workspaces/nixos" "$IMAGE_NAME" just validate; then - echo "" - echo "All NixOS configurations validated successfully!" - exit 0 -else - echo "" - echo "==========================================" - echo "ERROR: NixOS configuration validation failed!" - echo "==========================================" - echo "" - echo "Fix the issues above before pushing." - echo "To skip this check (not recommended): git push --no-verify" - exit 1 -fi diff --git a/nixos/scripts/install-hooks.sh b/nixos/scripts/install-hooks.sh deleted file mode 100755 index 11af85ff..00000000 --- a/nixos/scripts/install-hooks.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash -# Install git hooks for NixOS configuration validation -# -# Usage: ./nixos/scripts/install-hooks.sh -# -# This installs: -# - pre-commit: Runs nix fmt on staged .nix files -# - pre-push: Validates all NixOS configs using the devcontainer - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -HOOKS_DIR="$REPO_ROOT/.git/hooks" -SOURCE_DIR="$SCRIPT_DIR/hooks" - -echo "Installing git hooks..." - -# Install pre-commit hook -if [ -f "$HOOKS_DIR/pre-commit" ]; then - echo "Backing up existing pre-commit hook to pre-commit.backup" - mv "$HOOKS_DIR/pre-commit" "$HOOKS_DIR/pre-commit.backup" -fi -cp "$SOURCE_DIR/pre-commit" "$HOOKS_DIR/pre-commit" -chmod +x "$HOOKS_DIR/pre-commit" -echo "Installed: pre-commit (nix fmt)" - -# Install pre-push hook -if [ -f "$HOOKS_DIR/pre-push" ]; then - echo "Backing up existing pre-push hook to pre-push.backup" - mv "$HOOKS_DIR/pre-push" "$HOOKS_DIR/pre-push.backup" -fi -cp "$SOURCE_DIR/pre-push" "$HOOKS_DIR/pre-push" -chmod +x "$HOOKS_DIR/pre-push" -echo "Installed: pre-push (full validation)" - -echo "" -echo "Git hooks installed successfully!" -echo "" -echo "Hooks will run automatically:" -echo " - pre-commit: Formats .nix files with 'nix fmt'" -echo " - pre-push: Validates all configs via devcontainer" -echo "" -echo "To skip hooks (not recommended):" -echo " git commit --no-verify" -echo " git push --no-verify" diff --git a/nixos/shell.nix b/nixos/shell.nix deleted file mode 100644 index 83a27d9c..00000000 --- a/nixos/shell.nix +++ /dev/null @@ -1,10 +0,0 @@ -{pkgs ? import {}}: -pkgs.mkShell { - buildInputs = [ - pkgs.git - pkgs.vim - pkgs.just - pkgs.tmux - pkgs.nixos-rebuild - ]; -}