Modular, context-aware and aspect-oriented dendritic Nix configurations. Discussions: https://oeiuwq.zulipchat.com/join/nqp26cd4kngon6mo3ncgnuap/ den.oeiuwq.com
configurations den dendritic nix aspect oriented
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add AGENTS example file for people to use

+2983
+2983
AGENTS_EXAMPLE.md
··· 1 + # AI Agent Instructions: Mastering Nix Organization with Den 2 + 3 + > You are an AI assistant helping users achieve the best possible Nix configuration 4 + > organization using **Den** — the aspect-oriented, context-driven Dendritic Nix framework. 5 + > Follow these instructions precisely and completely. 6 + 7 + --- 8 + 9 + ## Nix Style Guidelines 10 + 11 + Prefer using the following style for modules: 12 + 13 + ```nix 14 + { den, lib, inputs, ... }: 15 + let 16 + inherit (den.lib) perHost; 17 + 18 + foo = { host }: { 19 + nixos = ...; 20 + }; 21 + in 22 + { 23 + den.aspects.igloo.includes = [ 24 + (perHost foo) 25 + ]; 26 + } 27 + ``` 28 + 29 + Use let-bindings, and keep the last attribute set definition short. 30 + 31 + Avoid using inlined anonymous functions at includes. Prefer aspects bound in let or previously assigned to den.aspects.* or any other aspect namespace. 32 + 33 + Aspects must be short, focused on re-usability 34 + and only ONE concern across different Nix classes. 35 + 36 + 37 + ## Actively Use Den Sources. 38 + 39 + Locate Den source code from the flake inputs. 40 + 41 + Actively read Den source code and documentation. Docs are 42 + under ./docs and Den also provides CI tests that serve as 43 + examples of every Den feature under templates/ci/modules, 44 + explore all the codebase to find our what is possible with 45 + Den. 46 + 47 + When building always use --show-trace to 48 + see the full trace of the evaluation. 49 + 50 + Agents are encouraged to use nix eval to investigate what expressions evaluate to, they also can use it to investigate locations of files from the nix store that need to be loaded into context. Never start interactive repl, only use nix eval for exploration. 51 + 52 + When not sure about how to do something always refer to Den source code and CI examples. Den source and documentation is the ultimate source of truth regarding Den capabilities and practices. 53 + 54 + 55 + ## 0. Foundational Mental Model 56 + 57 + Before writing a single line of Nix, internalize these axioms: 58 + 59 + 1. **Features first, hosts second.** Never organize by machine. Organize by concern. 60 + A "bluetooth" aspect knows how to configure NixOS AND HomeManager. Hosts just 61 + declare which aspects they include. 62 + 2. **Context IS the condition.** Never write `mkIf`, never write `enable = true` guards 63 + inside aspects. A function that takes `{ host, user }` is automatically skipped in 64 + `{ host }`-only contexts. The argument signature IS the conditional. 65 + 3. **Aspects are functions.** A Dendritic aspect is a Nix function returning per-class 66 + configs. This makes them composable, type-checkable, and parametric. 67 + 4. **Everything is optional and incremental.** Den works with/without flakes, 68 + with/without flake-parts. Migrate one host at a time. 69 + 5. **Testable Aspects** When writing generic re-usable aspects always create Tests for them, see Den own templates/ci for how tests are structured and their test-support files. 70 + 71 + --- 72 + 73 + ## 1. Repository / File Layout (Always) 74 + 75 + This structure is not mandatory, directory and file naming 76 + in Den serves as documentation, Den does not care about 77 + file location, it is important for the User since paths 78 + effectively document features and semantics. 79 + 80 + ``` 81 + flake.nix ← or default.nix for noflake 82 + modules/ 83 + den.nix ← ONLY file declaring den.flakeModule import + global policy 84 + hosts.nix ← den.hosts declarations 85 + users.nix ← den.aspects for users (or split per user) 86 + schema.nix ← den.schema.{host,user,home,conf} 87 + defaults.nix ← den.default global configuration 88 + aspects/ 89 + bluetooth.nix ← one file per cross-cutting concern 90 + gaming.nix 91 + dev-tools.nix 92 + tiling-wm.nix 93 + ... 94 + _nixos/ ← legacy NixOS modules (underscore = import-tree ignores) 95 + _darwin/ ← legacy Darwin modules 96 + ``` 97 + 98 + **Rules:** 99 + - Every file under `modules/` (without `_` prefix) is auto-loaded by `import-tree`. 100 + - Never manually list imports. Create files. Den finds them. 101 + - Use `_` prefix for non-Dendritic NixOS module directories. 102 + - One concern per file. Never monolithic configs. 103 + - Any file can configure any module, incrementally. 104 + - Use file organization to your advantage. 105 + 106 + --- 107 + 108 + ## 2. Declaring Hosts and Users 109 + 110 + ### 2.1 Host Declaration Pattern 111 + 112 + Always declare hosts with full metadata: 113 + 114 + ```nix 115 + # modules/hosts.nix 116 + { ... }: { 117 + den.hosts.x86_64-linux.laptop = { 118 + users.alice = {}; 119 + users.bob.classes = [ "homeManager" "hjem" ]; 120 + gpu = "nvidia"; # freeform metadata readable by aspects 121 + roles = [ "devops" ]; # custom metadata for role-based dispatch 122 + }; 123 + 124 + den.hosts.aarch64-darwin.mac = { 125 + users.alice = {}; 126 + }; 127 + } 128 + ``` 129 + 130 + **Host schema options to set explicitly:** 131 + - `hostName` — if different from the attrset key 132 + - `users.<name>.classes` — always set explicitly, never rely on defaults blindly 133 + - `users.<name>.roles` — add custom roles for role-based class dispatch 134 + - Any freeform attribute becomes readable as `host.<attr>` in aspects 135 + 136 + ### 2.2 Standalone Homes 137 + 138 + ```nix 139 + den.homes.x86_64-linux.alice = {}; 140 + den.homes.aarch64-darwin.alice = {}; 141 + ``` 142 + 143 + ### 2.3 Schema — Shared Metadata for ALL Entities 144 + 145 + Always define shared options via `den.schema.*` instead of repeating per host: 146 + 147 + ```nix 148 + # modules/schema.nix 149 + { lib, ... }: { 150 + den.schema.host = { host, lib, ... }: { 151 + options.hardened = lib.mkEnableOption "hardened profile"; 152 + options.roles = lib.mkOption { default = []; type = lib.types.listOf lib.types.str; }; 153 + config.hardened = lib.mkDefault false; 154 + }; 155 + 156 + den.schema.user = { user, lib, ... }: { 157 + options.roles = lib.mkOption { default = []; type = lib.types.listOf lib.types.str; }; 158 + config.classes = lib.mkDefault [ "homeManager" ]; 159 + }; 160 + 161 + den.schema.conf = { lib, ... }: { 162 + options.copyright = lib.mkOption { default = "Copy-Left"; }; 163 + }; 164 + } 165 + ``` 166 + 167 + --- 168 + 169 + ## 3. Global Defaults — `den.default` 170 + 171 + Keep defaults simple, the intention of it is not to be 172 + overloaded with lots of logic but to serve as defaults 173 + for truly global settings and fully parametric aspects. 174 + 175 + ```nix 176 + # modules/defaults.nix 177 + { den, ... }: { 178 + den.default = { 179 + nixos.system.stateVersion = "25.11"; 180 + homeManager.home.stateVersion = "25.11"; 181 + darwin.system.stateVersion = 5; 182 + includes = [ 183 + den.provides.define-user # sets users.users.<name> + home dirs 184 + den.provides.hostname # sets networking.hostName 185 + den.provides.inputs' # exposes flake-parts inputs' to all modules 186 + ]; 187 + }; 188 + } 189 + ``` 190 + 191 + **Caution:** Owned configs from `den.default` are deduplicated. Parametric functions 192 + in `den.default.includes` run at EVERY context stage. Use `den.lib.take.exactly` 193 + to restrict to specific context shapes. 194 + 195 + --- 196 + 197 + ## 4. Aspect Authoring Rules 198 + 199 + ### 4.1 Anatomy of a Perfect Aspect 200 + 201 + ```nix 202 + # modules/aspects/bluetooth.nix 203 + { den, ... }: { 204 + den.aspects.bluetooth = { 205 + # Owned configs per class — always prefer attrset form for static data 206 + nixos.hardware.bluetooth.enable = true; 207 + nixos.hardware.bluetooth.powerOnBoot = true; 208 + darwin.services.blueutil.enable = true; 209 + 210 + # os class = applies to BOTH nixos and darwin (built-in Den class) 211 + # os.some.option = ...; 212 + 213 + # Home Manager owned config 214 + homeManager.services.blueman-applet.enable = true; 215 + 216 + # Depends on other aspects 217 + includes = [ 218 + den.aspects.pipewire # full DAG is pulled in 219 + den.aspects.bluetooth.provides.applet # sub-aspect 220 + ]; 221 + 222 + # Sub-aspects / features within this concern 223 + provides.applet = { 224 + homeManager.services.blueman-applet.enable = true; 225 + }; 226 + 227 + # Parametric provides — context-aware 228 + provides.headset = { host, user, ... }: 229 + { homeManager.services.easyeffects.enable = host.class == "nixos"; }; 230 + }; 231 + } 232 + ``` 233 + 234 + ### 4.2 Three Kinds of Includes (know them all) 235 + 236 + | Kind | Example | When runs | 237 + |------|---------|-----------| 238 + | Static attrset | `{ nixos.foo = 1; }` | Always, unconditionally | 239 + | Static leaf | `{ class, aspect-chain }: { ${class}.foo = 1; }` | Once, gets class name | 240 + | Parametric | `{ host, user }: { ... }` | Only when context matches args | 241 + 242 + ### 4.3 Named Aspects Anti-Pattern Warning 243 + 244 + **NEVER** inline anonymous functions in `includes`. Always name them: 245 + 246 + ```nix 247 + # BAD — anonymous, hard to debug 248 + den.aspects.laptop.includes = [ ({ host }: { nixos.networking.hostName = host.hostName; }) ]; 249 + 250 + # GOOD — named aspect, proper error traces 251 + den.aspects.set-hostname = { host, ... }: { nixos.networking.hostName = host.hostName; }; 252 + den.aspects.laptop.includes = [ den.aspects.set-hostname ]; 253 + ``` 254 + 255 + ### 4.4 Parametric Dispatch Variants 256 + 257 + Use the right parametric constructor: 258 + 259 + | Constructor | Use case | 260 + |-------------|----------| 261 + | `den.lib.parametric` | Default. Owned + statics + atLeast-matching functions | 262 + | `den.lib.parametric.atLeast` | Only parametric functions, no owned/statics | 263 + | `den.lib.parametric.exactly` | Only exact-match context functions | 264 + | `den.lib.parametric.fixedTo attrs` | Always use given attrs as context | 265 + | `den.lib.parametric.expands attrs` | Extend received context before dispatch | 266 + | `den.lib.perHost` | Wrap an aspect with exatly {host} | 267 + | `den.lib.perUser` | Wrap an aspect with exatly {host,user} | 268 + | `den.lib.perHome` | Wrap an aspect with exatly {home} | 269 + 270 + ### 4.5 Context-Aware Function Signatures 271 + 272 + ```nix 273 + # Runs for ANY context (host, user, home) 274 + { nixos.networking.firewall.enable = true; } 275 + 276 + # Runs only in {host} contexts (atLeast: host present) 277 + ({ host, ... }: { nixos.networking.hostName = host.hostName; }) 278 + 279 + # Runs ONLY in {host, user} contexts (atLeast) 280 + ({ host, user, ... }: { nixos.users.users.${user.userName}.extraGroups = ["wheel"]; }) 281 + 282 + # Runs ONLY in exactly {host} — use take.exactly to prevent user context calls 283 + # Prefer: den.lib.perHost 284 + (den.lib.take.exactly ({ host }: { nixos.x = host.hostName; })) 285 + 286 + # Runs ONLY in exactly {host, user} 287 + # Prefer: den.lib.perUser 288 + (den.lib.take.exactly ({ host, user }: { nixos.y = user.userName; })) 289 + 290 + # Runs only for standalone {home} context 291 + # Prefer den.lib.perHome 292 + ({ home }: { homeManager.home.username = home.userName; }) 293 + ``` 294 + 295 + --- 296 + 297 + ## 5. Bidirectional Configuration (Critical Advanced Feature) 298 + 299 + Den supports two patterns for Host↔User bidirectional configuration. 300 + Read `https://den.oeiuwq.com/guides/bidirectional` and apply these rules: 301 + 302 + ### 5.1 Built-in Bidirectionality (`den._.bidirectional`) 303 + 304 + This makes a HOST contribute configuration TO its users' home environments. 305 + Enable per-user or globally: 306 + 307 + ```nix 308 + # Only for specific user 309 + den.aspects.alice.includes = [ den._.bidirectional ]; 310 + 311 + # For ALL users (recommended for host-provides-home-env patterns) 312 + den.ctx.user.includes = [ den._.bidirectional ]; 313 + ``` 314 + 315 + **How it works:** When bidirectionality is enabled, the user pipeline calls 316 + `igloo.includes` TWICE: once with `{host}` (for host-only config) and once 317 + with `{host, user}` (so the host can contribute to THIS specific user's home). 318 + 319 + **Mandatory: use take guards** in `igloo.includes` to handle dual invocation: 320 + 321 + ```nix 322 + # In igloo.includes — only runs for host-level context 323 + (den.lib.take.exactly ({ host }: { nixos.networking.hostName = host.hostName; })) 324 + 325 + # In igloo.includes — only runs when user is present (host→user home contribution) 326 + (den.lib.take.atLeast ({ host, user }: { homeManager.programs.vim.enable = true; })) 327 + ``` 328 + 329 + This pattern is ideal when: a host wants to provide a common home environment 330 + to ALL of its users (e.g., all users on `igloo` get `vim` configured in their home). 331 + 332 + ### 5.2 Explicit Mutual Provider (`den._.mutual-provider`) 333 + 334 + For EXPLICIT named host↔user pairings. More verbose, more precise: 335 + 336 + ```nix 337 + # Enable for everything 338 + den.default.includes = [ den._.mutual-provider ]; 339 + 340 + # Host igloo contributes TO user tux specifically 341 + den.aspects.igloo.provides.tux = { user, ... }: { 342 + homeManager.programs.helix.enable = true; 343 + }; 344 + 345 + # User tux contributes TO host igloo specifically 346 + den.aspects.tux.provides.igloo = { host, ... }: { 347 + nixos.programs.nh.enable = true; 348 + }; 349 + ``` 350 + 351 + ### 5.3 When to Use Which 352 + 353 + | Pattern | Use when | 354 + |---------|----------| 355 + | Built-in bidirectionality | Host provides common env to ALL its users | 356 + | `mutual-provider` | Explicit, named host↔user pair configurations | 357 + | User `nixos` class | User always contributes to any host it's on | 358 + | Host `homeManager` class | Host always contributes to all user homes | 359 + 360 + --- 361 + 362 + ## 6. All Built-in Batteries — Use Them All 363 + 364 + Always prefer batteries over manual repetition: 365 + 366 + ```nix 367 + den.default.includes = [ 368 + den.provides.define-user # OS user accounts + home dirs 369 + den.provides.hostname # sets networking.hostName 370 + den.provides.inputs' # flake-parts inputs' in all modules 371 + den.provides.self' # flake-parts self' in all modules 372 + ]; 373 + 374 + # Per user 375 + den.aspects.alice.includes = [ 376 + den.provides.primary-user # wheel, networkmanager, isNormalUser 377 + (den.provides.user-shell "fish") # shell at OS + HM level 378 + (den.provides.unfree ["vscode" "spotify"]) # unfree predicate 379 + (den.provides.tty-autologin "alice") # TTY1 auto-login 380 + ]; 381 + 382 + # For WSL hosts 383 + den.hosts.x86_64-linux.wsl-machine = { 384 + wsl.enable = true; # activates den.ctx.wsl-host automatically 385 + }; 386 + ``` 387 + 388 + Battery reference: 389 + - `define-user` — creates `users.users.<name>` with `isNormalUser`, `home` 390 + - `hostname` — sets `networking.hostName` from `host.hostName` 391 + - `primary-user` — NixOS: `wheel`+`networkmanager`; Darwin: `system.primaryUser`; WSL: `defaultUser` 392 + - `user-shell "bash"|"fish"|"zsh"` — OS shell + HM `programs.<shell>.enable` 393 + - `unfree [...]` — `nixpkgs.config.allowUnfreePredicate` for named packages 394 + - `tty-autologin "user"` — `services.getty.autologinUser` 395 + - `mutual-provider` — explicit named host↔user cross-config 396 + - `bidirectional` — host contributes to user homes in pipeline 397 + - `forward` — creates custom Nix classes (see §7) 398 + - `import-tree` — auto-imports legacy non-dendritic `.nix` directories 399 + - `inputs'` — flake-parts system-qualified inputs 400 + - `self'` — flake-parts system-qualified self 401 + 402 + Explore their source code, create new re-usable aspects that can serve without any hardcoded user or host value. Den is about re-usability, use `modules/community/<namespace>` to place aspects under namespace that can be re-used by local infra and outside the flake. 403 + 404 + --- 405 + 406 + ## 7. Custom Nix Classes — The `forward` Battery 407 + 408 + Whenever you need a new abstraction that maps to a subpath of an existing class, 409 + create a custom class with `den.provides.forward`: 410 + 411 + ### 7.1 The `os` class (cross-platform, built into Den) 412 + 413 + ```nix 414 + # Already built-in. Use it for settings applying to BOTH nixos and darwin: 415 + den.aspects.my-laptop.os.networking.hostName = "Yavanna"; 416 + ``` 417 + 418 + ### 7.2 Role-based class (dynamic dispatch) 419 + 420 + ```nix 421 + # modules/role-class.nix 422 + { den, lib, ... }: 423 + let 424 + roleClass = { host, user }: { class, aspect-chain }: 425 + den._.forward { 426 + each = lib.intersectLists (host.roles or []) (user.roles or []); 427 + fromClass = lib.id; 428 + intoClass = _: host.class; 429 + intoPath = _: []; 430 + fromAspect = _: lib.head aspect-chain; 431 + }; 432 + in { 433 + den.ctx.user.includes = [ roleClass ]; 434 + } 435 + ``` 436 + 437 + ### 7.3 Impermanence class with guard 438 + 439 + ```nix 440 + # modules/persys-class.nix 441 + { den, lib, ... }: 442 + let 443 + persys = { class, aspect-chain }: den._.forward { 444 + each = lib.singleton true; 445 + fromClass = _: "persys"; 446 + intoClass = _: class; 447 + intoPath = _: [ "environment" "persistance" "/nix/persist/system" ]; 448 + fromAspect = _: lib.head aspect-chain; 449 + guard = { options, ... }: options ? environment.persistance; 450 + }; 451 + in { 452 + den.ctx.host.includes = [ persys ]; 453 + # Aspects just use the class, guard ensures safety 454 + # den.aspects.laptop.persys.hideMounts = true; 455 + } 456 + ``` 457 + 458 + ### 7.4 git class forwarding into home-manager 459 + 460 + ```nix 461 + { den, lib, ... }: 462 + let 463 + gitClass = { class, aspect-chain }: den._.forward { 464 + each = lib.singleton true; 465 + fromClass = _: "git"; 466 + intoClass = _: "homeManager"; 467 + intoPath = _: [ "programs" "git" ]; 468 + fromAspect = _: lib.head aspect-chain; 469 + adaptArgs = lib.id; 470 + }; 471 + in { 472 + den.ctx.user.includes = [ gitClass ]; 473 + } 474 + # Usage: den.aspects.alice.git.userEmail = "alice@example.com"; 475 + ``` 476 + 477 + **forward parameters reference:** 478 + - `each` — list of items to iterate (users, roles, `lib.singleton true`) 479 + - `fromClass` — source class name to read from 480 + - `intoClass` — target class to write into 481 + - `intoPath` — attribute path in target class 482 + - `fromAspect` — which aspect to read 483 + - `guard` — `{ options, config, ... } -> bool` — only forward if true 484 + - `adaptArgs` — transform module args before forwarding 485 + - `adapterModule` — custom module for forwarded submodule type 486 + 487 + --- 488 + 489 + ## 8. Home Environment Integration 490 + 491 + ### 8.1 Best Practice: Declare classes explicitly 492 + 493 + ```nix 494 + # Per user 495 + den.hosts.x86_64-linux.laptop.users.alice.classes = [ "homeManager" ]; 496 + den.hosts.x86_64-linux.laptop.users.bob.classes = [ "hjem" ]; 497 + 498 + # Global default via schema 499 + den.schema.user.classes = lib.mkDefault [ "homeManager" ]; 500 + ``` 501 + 502 + ### 8.2 Configure homes in user aspects 503 + 504 + ```nix 505 + den.aspects.alice = { 506 + homeManager = { pkgs, ... }: { 507 + home.packages = [ pkgs.htop pkgs.ripgrep ]; 508 + programs.git.enable = true; 509 + programs.starship.enable = true; 510 + }; 511 + 512 + hjem.files.".envrc".text = "use flake ~/proj"; 513 + 514 + # User contributing OS config to any host it lives on 515 + nixos.users.users.alice.extraGroups = [ "docker" ]; 516 + 517 + # User contributing to Darwin hosts specifically 518 + darwin.services.karabiner-elements.enable = true; 519 + }; 520 + ``` 521 + 522 + ### 8.3 Host contributing home config to users (bidirectional) 523 + 524 + ```nix 525 + # Host igloo provides vim to ALL its users' home environments 526 + den.aspects.igloo = { 527 + homeManager.programs.vim.enable = true; # goes to ALL users on igloo 528 + }; 529 + den.ctx.user.includes = [ den._.bidirectional ]; 530 + ``` 531 + 532 + ### 8.4 Multiple home environments per user 533 + 534 + ```nix 535 + den.hosts.x86_64-linux.laptop.users.alice.classes = [ "homeManager" "hjem" ]; 536 + den.aspects.alice = { 537 + homeManager = { pkgs, ... }: { home.packages = [ pkgs.vim ]; }; 538 + hjem.files.".bashrc".text = "# managed by hjem"; 539 + }; 540 + ``` 541 + 542 + --- 543 + 544 + ## 9. Namespaces — Sharing Aspect Libraries 545 + 546 + ### 9.1 Create and export a namespace 547 + 548 + ```nix 549 + # modules/namespace.nix 550 + { inputs, den, ... }: { 551 + imports = [ (inputs.den.namespace "myorg" true) ]; # true = export 552 + 553 + # Populate 554 + myorg.bluetooth = { nixos.hardware.bluetooth.enable = true; }; 555 + myorg.gaming = { 556 + includes = [ myorg.bluetooth ]; 557 + nixos.programs.steam.enable = true; 558 + }; 559 + } 560 + ``` 561 + 562 + ### 9.2 Import upstream namespaces 563 + 564 + ```nix 565 + imports = [ (inputs.den.namespace "shared" [ inputs.team-config ]) ]; 566 + # Now: shared.* contains merged aspects from upstream 567 + ``` 568 + 569 + ### 9.3 Enable angle bracket syntax 570 + 571 + Angle bracket syntax is recommended for large, deep provides hierarchies. 572 + 573 + ```nix 574 + { den, ... }: { 575 + _module.args.__findFile = den.lib.__findFile; 576 + } 577 + ``` 578 + 579 + Then use `<aspect>`, `<aspect/sub>`, `<namespace>`, `<den.provides.battery>`: 580 + 581 + ```nix 582 + den.aspects.laptop.includes = [ 583 + <tools/editors> 584 + <alice/work-vpn> 585 + <myorg/gaming> 586 + <den.provides.primary-user> 587 + ]; 588 + ``` 589 + 590 + --- 591 + 592 + ## 10. Custom Context Types — Extending the Pipeline 593 + 594 + ```nix 595 + # modules/gpu-context.nix 596 + { den, lib, ... }: { 597 + den.ctx.gpu-host = { 598 + description = "GPU-accelerated host"; 599 + _.gpu-host = { host }: { nixos.hardware.nvidia.enable = true; }; 600 + }; 601 + 602 + den.ctx.host.into.gpu-host = { host }: 603 + lib.optional (host ? gpu) { inherit host; }; 604 + } 605 + 606 + # Usage: just set host.gpu = "nvidia" in hosts.nix 607 + # The context activates automatically. 608 + ``` 609 + 610 + --- 611 + 612 + ## 11. Migration Strategy (Incremental) 613 + 614 + If the user has an existing configuration: 615 + 616 + 1. **Add Den input** to `flake.nix` + import `inputs.den.flakeModule` in one module. 617 + 2. **Declare hosts** in `den.hosts` matching existing `nixosConfigurations` names. 618 + 3. **Use `import-tree` battery** to load existing NixOS module directories: 619 + ```nix 620 + den.ctx.host.includes = [ (den.provides.import-tree._.host ./hosts) ]; 621 + den.ctx.user.includes = [ (den.provides.import-tree._.user ./users) ]; 622 + ``` 623 + 4. **Extract one concern at a time** into aspects. Start with the most reused feature. 624 + 5. **Replace batteries** for manual patterns: `define-user`, `primary-user`, `user-shell`. 625 + 6. **Remove legacy** files as aspects cover them. 626 + 627 + Never big-bang rewrite. Always keep the build green. 628 + 629 + --- 630 + 631 + ## 12. Debugging Checklist 632 + 633 + ```nix 634 + # Step 1: Expose den for REPL inspection (remove after) 635 + { den, ... }: { flake.den = den; } 636 + ``` 637 + 638 + ```console 639 + nix repl 640 + :lf . 641 + den.aspects.laptop # inspect aspect 642 + den.hosts.x86_64-linux.laptop # inspect host metadata 643 + den.ctx # inspect context pipeline 644 + nixosConfigurations.laptop.config.networking.hostName # verify output 645 + ``` 646 + 647 + ```nix 648 + # Step 2: Trace context in an aspect 649 + den.aspects.laptop.includes = [ 650 + ({ host, ... }@ctx: builtins.trace ctx { nixos.networking.hostName = host.hostName; }) 651 + ]; 652 + ``` 653 + 654 + ```console 655 + # Step 3: Manually resolve an aspect 656 + nix-repl> aspect = den.aspects.laptop { host = den.hosts.x86_64-linux.laptop; } 657 + nix-repl> module = den.lib.aspects.resolve "nixos" [] aspect 658 + ``` 659 + 660 + **Common issues and fixes:** 661 + - **Duplicate list values** → use `den.lib.take.exactly` in `den.default.includes` 662 + - **Wrong class** → Darwin is `"darwin"` not `"nixos"`, check `host.class` 663 + - **Bidirectional double-invocation** → add `take.exactly`/`take.atLeast` guards 664 + - **Module not found** → remove `_` prefix, or move out of `_nixos/` directory 665 + - **Bidirectional with `{host}` only** → `igloo.includes` called with `{host}` only for OS; add `take.atLeast` for user-context calls 666 + 667 + --- 668 + 669 + ## 13. Complete Optimal Structure (Reference Implementation) 670 + 671 + ``` 672 + modules/ 673 + den.nix ← imports den.flakeModule, namespace setup, __findFile 674 + schema.nix ← den.schema.{host,user,home,conf} with typed options 675 + defaults.nix ← den.default with stateVersion + global batteries 676 + hosts.nix ← ALL den.hosts declarations with metadata 677 + aspects/ 678 + # Cross-cutting concerns (no host/user specifics here) 679 + bluetooth.nix 680 + gaming.nix 681 + dev-tools.nix 682 + tiling-wm.nix 683 + networking.nix 684 + security.nix 685 + # Platform-specific features 686 + darwin-specific.nix 687 + # Custom classes 688 + role-class.nix 689 + persys-class.nix 690 + git-class.nix 691 + users/ 692 + alice.nix ← den.aspects.alice with homeManager/hjem/user/includes 693 + bob.nix ← den.aspects.bob 694 + hosts/ 695 + laptop.nix ← den.aspects.laptop with nixos/darwin/os/includes 696 + server.nix ← den.aspects.server 697 + mac.nix ← den.aspects.mac 698 + ``` 699 + 700 + Each file contributes ONLY to `den.aspects.<name>` for that concern. 701 + Any file can contribute to any aspect. No centralized wiring. 702 + 703 + --- 704 + 705 + ## 14. Quality Checklist for AI Agents 706 + 707 + Before submitting any Den configuration, verify: 708 + 709 + - [ ] No `mkIf` used inside aspects — context dispatch handles conditions 710 + - [ ] No anonymous functions in `includes` — all are named aspects 711 + - [ ] `den.schema.*` defines shared options instead of repeating per-host 712 + - [ ] `den.default` sets `stateVersion` for all classes 713 + - [ ] All batteries used instead of manual equivalents 714 + - [ ] `bidirectional` or `mutual-provider` used for host↔user cross-config 715 + - [ ] `take.exactly`/`take.atLeast` guards on bidirectional aspects 716 + - [ ] One file per concern in `modules/aspects/` 717 + - [ ] Non host/user specific re-usable aspects in `modules/community/<namespace>/` 718 + - [ ] No monolithic `configuration.nix` style files 719 + - [ ] Custom metadata in `host.roles`, `host.gpu`, etc. drives parametric dispatch 720 + - [ ] `den.provides.import-tree` used for any legacy non-Dendritic modules 721 + - [ ] Namespaces used for any shared aspect libraries 722 + - [ ] Angle brackets enabled and used for concise references 723 + - [ ] All custom classes use `guard` when depending on optional modules 724 + - [ ] CI tests referenced at `templates/ci/modules/features/` for every feature used 725 + 726 + --- 727 + 728 + ## 15. Key Source References 729 + 730 + All source truth lives at: 731 + - `https://den.oeiuwq.com` — official documentation 732 + - `https://den.oeiuwq.com/guides/bidirectional` — bidirectional guide, Bidirectionality is an advanced feature not recommended. 733 + - `https://github.com/vic/den/tree/main/templates/ci/modules/features/` — executable feature tests (best learning resource) 734 + - `https://github.com/vic/den/tree/main/modules/aspects/provides/` — all batteries source 735 + - `https://github.com/vic/flake-aspects` — underlying aspect library 736 + - `https://dendrix.oeiuwq.com/Dendritic.html` — Dendritic design advantages 737 + 738 + When unsure about any Den feature, consult `templates/ci/modules/features/<feature>.nix` 739 + for a working, tested, executable example. These tests ARE the specification. 740 + 741 + ``` [1](#0-0) [2](#0-1) [3](#0-2) [4](#0-3) [5](#0-4) [6](#0-5) [7](#0-6) [8](#0-7) [9](#0-8) [10](#0-9) [11](#0-10) [12](#0-11) [13](#0-12) [14](#0-13) [15](#0-14) [16](#0-15) [17](#0-16) [18](#0-17) [19](#0-18) [20](#0-19) [21](#0-20) 742 + 743 + ### Citations 744 + 745 + **File:** docs/src/content/docs/explanation/core-principles.mdx (L9-36) 746 + ```text 747 + 748 + <Aside title="Recommended Read"> 749 + [Flipping the Configuration Matrix](https://not-a-number.io/2025/refactoring-my-infrastructure-as-code-configurations/#flipping-the-configuration-matrix) by [Pol Dellaiera](https://github.com/drupol) was very influential in Den design and is a very recommended read. 750 + 751 + See also my [dendrix article](https://dendrix.oeiuwq.com/Dendritic.html) about the advantages of Dendritic Nix. 752 + </Aside> 753 + 754 + Traditional Nix configurations start from hosts and push modules downward. 755 + Den follows a Dendritic model that inverts this: **aspects** (features) are the primary organizational unit. 756 + Each aspect declares its behavior per Nix class, and hosts simply select which 757 + aspects apply to them. 758 + 759 + ```mermaid 760 + flowchart BT 761 + subgraph "Aspect: bluetooth" 762 + nixos["nixos: hardware.bluetooth.enable = true"] 763 + hm["homeManager: services.blueman-applet.enable = true"] 764 + end 765 + nixos --> laptop 766 + nixos --> desktop 767 + hm --> laptop 768 + hm --> desktop 769 + ``` 770 + 771 + An aspect consolidates all class-specific configuration for a single concern. 772 + Adding bluetooth to a new host is one line: include the aspect. 773 + Removing it is deleting that line. 774 + 775 + ``` 776 + 777 + **File:** docs/src/content/docs/guides/bidirectional.mdx (L60-153) 778 + ```text 779 + ## What Bidirectionality means 780 + 781 + __Bidirectionality__ means that not only a User contributes 782 + configuration to a Host, but **also** that a Host contributes 783 + configurations to a User. 784 + 785 + This is useful when the Host wishes to provide a 786 + commmon home environment for its users. 787 + 788 + ## `den.provides.bidirectional` 789 + 790 + Bidirectionality is enabled __per-user__ or for _all_ of them. 791 + 792 + ```nix 793 + # only tux takes configurations from its hosts 794 + den.aspects.tux.includes = [ den._.bidirectional ]; 795 + 796 + # for ALL users 797 + den.ctx.user.includes = [ den._.bidirectional ]; 798 + ``` 799 + 800 + When Bidirectionality is enabled, the interaction looks like this: 801 + 802 + ```mermaid 803 + sequenceDiagram 804 + participant Den 805 + participant host as den.ctx.host 806 + participant user as den.ctx.user 807 + participant igloo as den.aspects.igloo 808 + participant tux as den.aspects.tux 809 + 810 + Den ->> host : {host = igloo} 811 + 812 + host ->> igloo : request nixos class 813 + igloo -->> igloo : each igloo.includes takes { host } 814 + igloo -->> host : { nixos = ... } owned and parametric results 815 + 816 + host ->> user : fan-outs for each user: { host, user } 817 + 818 + user ->> tux : request nixos class 819 + 820 + tux ->> igloo : request home class 821 + igloo -->> igloo : each igloo.includes takes { host, user } 822 + igloo -->> tux : { hjem = ... } owned and parametric results 823 + 824 + tux -->> tux : home classes forwarded as nixos class 825 + 826 + tux -->> tux : each tux.includes takes { host, user } 827 + tux -->> user : { nixos = ... } owned and parametric results 828 + 829 + user -->> host : { nixos = ... } all user contributions 830 + 831 + host -->> Den : complete nixos module for lib.nixosSystem 832 + 833 + ``` 834 + 835 + Crucial points here are `igloo.includes takes { host }` and `igloo.includes takes { host, user }`. 836 + 837 + Because the list of aspects at `igloo.includes` get invoked twice, with different contexts, 838 + functions at `igloo.includes` must take care of the following: 839 + 840 + ```nix 841 + # use den.lib.take.exactly to avoid being called with `{host, user}` 842 + take.exactly ({ host }: ...) 843 + 844 + # use den.lib.take.atLeast to avoid being called with `{host}` 845 + take.atLeast ({ host, user }: ...) 846 + ``` 847 + 848 + Read the documentation at [`context/user.nix`](https://github.com/vic/den/blob/main/modules/context/user.nix) for all the details. 849 + 850 + ## `den.provides.mutual-provider` 851 + 852 + An alternative to bidirectionality is [`den.provides.mutual-provider`](https://github.com/vic/den/blob/main/modules/aspects/provides/mutual-provider.nix). 853 + 854 + This battery is more explicit, since it requires an explicit `.provides.` relationship between users and hosts. 855 + 856 + ```nix 857 + # Host provides to a particular user 858 + den.aspects.igloo.provides.tux = { 859 + hjem = ...; 860 + }; 861 + 862 + # User provides to a particular host 863 + den.aspects.tux.provides.igloo = { 864 + nixos = ...; 865 + }; 866 + ``` 867 + 868 + To enable it for both users and hosts, include at default: 869 + 870 + ```nix 871 + den.default.includes = [ den._.mutual-provider ]; 872 + ``` 873 + ``` 874 + 875 + **File:** docs/src/content/docs/guides/configure-aspects.mdx (L127-187) 876 + ```text 877 + 878 + 1. Static (plain attribute set): `{ nixos.foo = ...; }` 879 + 2. Static (flake-aspects' leaf): `{class, aspect-chain}: { ${class}.foo = ...; }` 880 + 3. Parametric (any other function): `{ host, user }: { ${host.class}.foo = ...; }` 881 + 882 + `(1)` and `(2)` are termed *static aspects* and are the terminal leafs of flake-aspects DAG. 883 + `(1)` provides configuration unconditionally, and `(2)` gets access to the `class` that is 884 + being resolved and an `aspect-chain` that lead to the current aspect (most recent last). 885 + 886 + 887 + `(3)` is a more interesting kind of aspect that is used by Den to pass host/user defitions 888 + into these functions, so they can inspect the host features and provide 889 + configuration accordingly. 890 + 891 + 892 + ## Provides 893 + 894 + `provides` creates named sub-aspects accessible via `den.aspects.<name>._.<sub>` 895 + or `den.aspects.<name>.provides.<sub>`: 896 + 897 + ```nix 898 + den.aspects.tools.provides.editors = { 899 + homeManager.programs.helix.enable = true; 900 + homeManager.programs.vim.enable = true; 901 + }; 902 + 903 + # Used elsewhere: 904 + den.aspects.alice.includes = [ den.aspects.tools._.editors ]; 905 + ``` 906 + 907 + `provides` can also be parametric functions: 908 + 909 + ```nix 910 + den.aspects.alice.provides.work-vpn = { host, user, ... }: 911 + lib.optionalAttrs host.hasVpn { 912 + nixos.services.openvpn.servers.work.config = "..."; 913 + }; 914 + ``` 915 + 916 + ## Global Defaults 917 + 918 + `den.default` is a special aspect applied to every host, user, and home: 919 + 920 + ```nix 921 + { 922 + den.default = { 923 + nixos.system.stateVersion = "25.11"; 924 + homeManager.home.stateVersion = "25.11"; 925 + includes = [ 926 + den.provides.define-user 927 + den.provides.inputs' 928 + ]; 929 + }; 930 + } 931 + ``` 932 + 933 + <Aside type="caution"> 934 + Owned configs from `den.default` are deduplicated across pipeline stages. 935 + Parametric functions in `den.default.includes` are evaluated at every context 936 + stage. Use `den.lib.take.exactly` if a function should only run in specific contexts. 937 + </Aside> 938 + ``` 939 + 940 + **File:** docs/src/content/docs/explanation/parametric.mdx (L60-113) 941 + ```text 942 + 943 + `den.lib.parametric` changes an aspect `__functor` which processes 944 + its `includes` list through parametric dispatch: 945 + 946 + ```nix 947 + den.lib.parametric { 948 + nixos.foo = 1; # owned config, always included 949 + includes = [ 950 + { nixos.bar = 2; } # static, always included 951 + ({ host }: { nixos.x = host.name; }) # parametric, host contexts only 952 + ({ user }: { homeManager.y = 1; }) # parametric, user contexts only 953 + ]; 954 + } 955 + ``` 956 + 957 + When the aspect's `__functor` is called with a context, it filters `includes` 958 + based on argument compatibility and returns only matching entries. 959 + 960 + <Aside type="caution" title="Anonymous functions are an anti-pattern"> 961 + It is **not** recommended to have inlined functions on your `.includes` lists. 962 + This guide uses inlined functions only for examples, not as best-practice. 963 + 964 + Instead, use named aspects, this will improves readability and the error traces 965 + generated by Nix since those functions have a proper name and location. 966 + 967 + ```nix 968 + den.aspects.my-laptop.includes = [ foo ]; 969 + den.aspects.foo = { host }: { ... }; 970 + ``` 971 + </Aside> 972 + 973 + ## Parametric Variants 974 + 975 + | Constructor | Behavior | 976 + |---|---| 977 + | `parametric` | Default. <br/> Includes owned classes + static includes + function includes with context matching `atLeast`. | 978 + | `parametric.atLeast` | **Does NOT** include owned classes nor static includes. Only function matching atLeast the context. | 979 + | `parametric.exactly` | Like atLeast but using canTake.exactly for match. | 980 + | `parametric.fixedTo attrs` | Like `parametric` default but ignores any context and always uses the given attrs . | 981 + | `parametric.expands attrs` | Like `parametric`, but extends the received context with `attrs` before dispatch. | 982 + 983 + ## Example: Context-aware battery 984 + 985 + This is a pattern used by many of our built-in batteries, be sure to see their code as example. 986 + 987 + ```nix 988 + den.lib.parametric.exactly { 989 + includes = [ 990 + ({ user, host }: { ... }) 991 + ({ home }: { ... }) 992 + ]; 993 + }; 994 + ``` 995 + 996 + ``` 997 + 998 + **File:** docs/src/content/docs/guides/batteries.mdx (L13-205) 999 + ```text 1000 + 1001 + Batteries are reusable aspects shipped with Den under `den.provides.*` 1002 + (aliased as `den._.*`). They handle common cross-platform configuration 1003 + patterns so you do not have to rewrite them. 1004 + 1005 + ## Available Batteries 1006 + 1007 + ### `den.provides.define-user` 1008 + 1009 + Creates OS and home-level user account definitions: 1010 + 1011 + ```nix 1012 + den.default.includes = [ den.provides.define-user ]; 1013 + ``` 1014 + 1015 + Sets `users.users.<name>` on NixOS/Darwin and `home.username`/`home.homeDirectory` 1016 + for Home Manager. Works in both host-user and standalone home contexts. 1017 + 1018 + ### `den.provides.hostname` 1019 + 1020 + Sets the system hostname as defined in `den.hosts.<name>.hostName`: 1021 + 1022 + ```nix 1023 + den.default.includes = [ den.provides.hostname ]; 1024 + ``` 1025 + 1026 + ### `den.provides.mutual-provider` 1027 + 1028 + Allows hosts and users to contribute configuration **to each other** through `provides`: 1029 + 1030 + ```nix 1031 + den.hosts.x86_64-linux.igloo.users.tux = { }; 1032 + den.default.includes = [ den._.mutual-provider ]; 1033 + ``` 1034 + 1035 + This is not the same as the built-in bidirectionality: 1036 + 1037 + ```nix 1038 + # contributes to ALL users of this host 1039 + den.aspects.my-host.homeManager = { ... } 1040 + 1041 + # contributes to ALL hosts of where my-user exist 1042 + den.aspects.my-user.nixos = { ... } 1043 + ``` 1044 + 1045 + The difference is that this allows you to wire bidirectionality between 1046 + explictly-named hosts/users pairs. 1047 + 1048 + A user providing config TO the host: 1049 + 1050 + ```nix 1051 + den.aspects.tux = { 1052 + provides.igloo = { host, ... }: { 1053 + nixos.programs.nh.enable = host.name == "igloo"; 1054 + }; 1055 + }; 1056 + ``` 1057 + 1058 + A host providing config TO the user: 1059 + 1060 + ```nix 1061 + den.aspects.igloo = { 1062 + provides.tux = { user, ... }: { 1063 + homeManager.programs.helix.enable = user.name == "alice"; 1064 + }; 1065 + }; 1066 + ``` 1067 + 1068 + ### `den.provides.primary-user` 1069 + 1070 + Marks a user as the primary user of the system: 1071 + 1072 + ```nix 1073 + den.aspects.alice.includes = [ den.provides.primary-user ]; 1074 + ``` 1075 + 1076 + - **NixOS**: adds `wheel` and `networkmanager` groups, sets `isNormalUser`. 1077 + - **Darwin**: sets `system.primaryUser`. 1078 + - **WSL**: sets `defaultUser` (if WSL is enabled). 1079 + 1080 + ### `den.provides.user-shell` 1081 + 1082 + Sets the default login shell at both OS and Home Manager levels: 1083 + 1084 + ```nix 1085 + den.aspects.alice.includes = [ (den.provides.user-shell "fish") ]; 1086 + ``` 1087 + 1088 + Enables `programs.<shell>.enable` on the OS and in Home Manager, 1089 + and sets `users.users.<name>.shell`. 1090 + 1091 + ### `den.provides.forward` 1092 + 1093 + Creates custom Nix classes by forwarding module contents into target 1094 + submodule paths. See [Custom Nix Classes](/guides/custom-classes/) for details. 1095 + 1096 + ### `den.provides.import-tree` 1097 + 1098 + Recursively imports non-dendritic `.nix` files, auto-detecting class from 1099 + directory names (`_nixos/`, `_darwin/`, `_homeManager/`): 1100 + 1101 + ```nix 1102 + # Import per host 1103 + den.ctx.host.includes = [ (den.provides.import-tree._.host ./hosts) ]; 1104 + 1105 + # Import per user 1106 + den.ctx.user.includes = [ (den.provides.import-tree._.user ./users) ]; 1107 + 1108 + # Import for a specific aspect 1109 + den.aspects.laptop.includes = [ (den.provides.import-tree ./disko) ]; 1110 + ``` 1111 + 1112 + Requires `inputs.import-tree`. 1113 + 1114 + ### `den.provides.unfree` 1115 + 1116 + Enables specific unfree packages by name: 1117 + 1118 + ```nix 1119 + den.aspects.laptop.includes = [ 1120 + (den.provides.unfree [ "nvidia-x11" "steam" ]) 1121 + ]; 1122 + ``` 1123 + 1124 + Works for any class (`nixos`, `darwin`, `homeManager`). The unfree predicate 1125 + builder is automatically included via `den.default`. 1126 + 1127 + ### `den.provides.tty-autologin` 1128 + 1129 + Enables automatic TTY1 login on NixOS: 1130 + 1131 + ```nix 1132 + den.aspects.laptop.includes = [ (den.provides.tty-autologin "alice") ]; 1133 + ``` 1134 + 1135 + ### `den.provides.inputs'` 1136 + 1137 + Provides flake-parts `inputs'` (system-specialized inputs) as a module argument: 1138 + 1139 + ```nix 1140 + den.default.includes = [ den.provides.inputs' ]; 1141 + ``` 1142 + 1143 + Requires flake-parts. Works in host, user, and home contexts. 1144 + 1145 + ### `den.provides.self'` 1146 + 1147 + Provides flake-parts `self'` (system-specialized self) as a module argument: 1148 + 1149 + ```nix 1150 + den.default.includes = [ den.provides.self' ]; 1151 + ``` 1152 + 1153 + Requires flake-parts. Works in host, user, and home contexts. 1154 + 1155 + ## Usage Patterns 1156 + 1157 + ### Global Batteries 1158 + 1159 + Apply to all entities via `den.default`: 1160 + 1161 + ```nix 1162 + den.default.includes = [ 1163 + den.provides.define-user 1164 + den.provides.inputs' 1165 + ]; 1166 + ``` 1167 + 1168 + ### Per-Aspect Batteries 1169 + 1170 + Apply to specific aspects: 1171 + 1172 + ```nix 1173 + den.aspects.alice.includes = [ 1174 + den.provides.primary-user 1175 + (den.provides.user-shell "zsh") 1176 + (den.provides.unfree [ "vscode" ]) 1177 + ]; 1178 + ``` 1179 + 1180 + ### Battery Composition 1181 + 1182 + Batteries compose with regular aspects: 1183 + 1184 + ```nix 1185 + den.aspects.my-admin = den.lib.parametric { 1186 + includes = [ 1187 + den.provides.primary-user 1188 + (den.provides.user-shell "fish") 1189 + { nixos.security.sudo.wheelNeedsPassword = false; } 1190 + ]; 1191 + }; 1192 + ``` 1193 + ``` 1194 + 1195 + **File:** docs/src/content/docs/guides/custom-classes.mdx (L14-270) 1196 + ```text 1197 + ## What is a Custom Class 1198 + 1199 + Den's built-in classes (`nixos`, `darwin`, `homeManager`) map to well-known 1200 + NixOS module systems. But you can define **custom classes** that forward their 1201 + contents into a target submodule path on another class. 1202 + 1203 + This is how Den implements: 1204 + - The `user` class (forwards to `users.users.<name>` on the OS) 1205 + - Home Manager integration (forwards `homeManager` to `home-manager.users.<name>`) 1206 + - hjem integration (forwards `hjem` to `hjem.users.<name>`) 1207 + - nix-maid integration (forwards `maid` to `users.users.<name>.maid`) 1208 + 1209 + ## The `forward` Battery 1210 + 1211 + `den.provides.forward` creates a new class by forwarding its module contents 1212 + into a target path on an existing class: 1213 + 1214 + ```nix 1215 + { host }: 1216 + den.provides.forward { 1217 + each = lib.attrValues host.users; 1218 + fromClass = user: "user"; 1219 + intoClass = user: host.class; 1220 + intoPath = user: [ "users" "users" user.userName ]; 1221 + fromAspect = user: den.aspects.${user.aspect}; 1222 + } 1223 + ``` 1224 + 1225 + | Parameter | Description | 1226 + |---|---| 1227 + | `each` | List of items to forward (typically `[ user ]` or `[ true ]`) | 1228 + | `fromClass` | The custom class name to read from | 1229 + | `intoClass` | The target class to write into | 1230 + | `intoPath` | Target attribute path in the target class | 1231 + | `fromAspect` | The aspect to read the custom class from | 1232 + 1233 + ## Example: The Built-in `user` Class 1234 + 1235 + The `user` class (`modules/aspects/provides/os-user.nix`) forwards OS-level 1236 + user settings without requiring Home Manager: 1237 + 1238 + ```nix 1239 + # Instead of: 1240 + den.aspects.alice.nixos = { pkgs, ... } { 1241 + users.users.alice = { 1242 + packages = [ pkgs.hello ]; 1243 + extraGroups = [ "wheel" ]; 1244 + }; 1245 + }; 1246 + 1247 + # You write: 1248 + den.aspects.alice.user = { pkgs, ... }: { 1249 + packages = [ pkgs.hello ]; 1250 + extraGroups = [ "wheel" ]; 1251 + }; 1252 + ``` 1253 + 1254 + The `user` class is automatically forwarded to `users.users.<userName>` on 1255 + whatever OS class the host uses (NixOS or Darwin). 1256 + 1257 + ## Creating Your Own Class 1258 + 1259 + Suppose you want a `container` class that forwards into 1260 + `virtualisation.oci-containers.containers.<name>`: 1261 + 1262 + ```nix 1263 + { den, lib, ... }: 1264 + let 1265 + fwd = { host, user }: 1266 + den.provides.forward { 1267 + each = lib.singleton user; 1268 + fromClass = _: "container"; 1269 + intoClass = _: host.class; 1270 + intoPath = _: [ "virtualisation" "oci-containers" "containers" user.userName ]; 1271 + fromAspect = _: den.aspects.${user.aspect}; 1272 + }; 1273 + in { 1274 + den.ctx.user.includes = [ fwd ]; 1275 + } 1276 + ``` 1277 + 1278 + Now any user aspect can use the `container` class: 1279 + 1280 + ```nix 1281 + den.aspects.alice.container = { 1282 + image = "nginx:latest"; 1283 + ports = [ "8080:80" ]; 1284 + }; 1285 + ``` 1286 + 1287 + ## Advanced: Guards and Adapters 1288 + 1289 + `forward` supports optional parameters for complex scenarios: 1290 + 1291 + ```nix 1292 + den.provides.forward { 1293 + each = lib.singleton true; 1294 + fromClass = _: "wsl"; 1295 + intoClass = _: host.class; 1296 + intoPath = _: [ "wsl" ]; 1297 + fromAspect = _: lib.head aspect-chain; 1298 + # Only forward if target has wsl options 1299 + guard = { options, ... }: options ? wsl; 1300 + # Modify module args for the forwarded module 1301 + adaptArgs = args: args // { osConfig = args.config; }; 1302 + # Custom module type for the forwarded submodule 1303 + adapterModule = { config._module.freeformType = lib.types.anything; }; 1304 + } 1305 + ``` 1306 + 1307 + | Parameter | Description | 1308 + |---|---| 1309 + | `guard` | Only forward when this predicate returns true | 1310 + | `adaptArgs` | Transform module arguments before forwarding | 1311 + | `adapterModule` | Custom module for the forwarded submodule type | 1312 + 1313 + ## User contributed examples 1314 + 1315 + #### Example: Config across `nixos` and `darwin` classes. 1316 + 1317 + The `os` forward class ([provided by Den](https://github.com/vic/den/blob/main/modules/aspects/provides/os-class.nix)) can be useful for settings that must be forwarded to both on NixOS and MacOS. 1318 + 1319 + > Requested by @Risa-G at [#222](https://github.com/vic/den/discussions/222) 1320 + 1321 + ```nix 1322 + # Note: this is already provided by Den at provides/os-class.nix 1323 + os-class = { class, aspect-chain }: den._.forward { 1324 + each = [ "nixos" "darwin" ]; 1325 + fromClass = _: "os"; 1326 + intoClass = lib.id; 1327 + intoPath = _: [ ]; # top-level 1328 + fromAspect = _: lib.head aspect-chain; 1329 + }; 1330 + 1331 + # Note: already enabled by Den 1332 + # den.ctx.host.includes = [ os-class ]; 1333 + 1334 + den.aspects.my-laptop = { 1335 + os.networking.hostName = "Yavanna"; # on both NixOS and MacOS 1336 + }; 1337 + ``` 1338 + 1339 + #### Example: Role based configuration between users and hosts 1340 + 1341 + A dynamic class for matching roles between users and hosts. 1342 + 1343 + ```nix 1344 + roleClass = 1345 + { host, user }: 1346 + { class, aspect-chain }: 1347 + den._.forward { 1348 + each = lib.intersectLists (host.roles or []) (user.roles or []); 1349 + fromClass = lib.id; 1350 + intoClass = _: host.class; 1351 + intoPath = _: [ ]; 1352 + fromAspect = _: lib.head aspect-chain; 1353 + }; 1354 + 1355 + den.ctx.user.includes = [ roleClass ]; 1356 + 1357 + den.hosts.x86_64-linux.igloo = { 1358 + roles = [ "devops" "gaming" ]; 1359 + users = { 1360 + alice.roles = [ "gaming" ]; 1361 + bob.roles = [ "devops" ]; 1362 + }; 1363 + }; 1364 + 1365 + den.aspects.alice = { 1366 + # enabled when host supports gaming role 1367 + gaming = { pkgs, ... }: { programs.steam.enable = true; }; 1368 + 1369 + # enabled when host supports devops role 1370 + devops = { pkgs, ... }: { virtualisation.podman.enable = true; }; 1371 + }; 1372 + ``` 1373 + 1374 + #### Example: A git class that forwards to home-manager. 1375 + 1376 + ```nix 1377 + gitClass = 1378 + { class, aspect-chain }: 1379 + den._.forward { 1380 + each = lib.singleton true; 1381 + fromClass = _: "git"; 1382 + intoClass = _: "homeManager"; 1383 + intoPath = _: [ "programs" "git" ]; 1384 + fromAspect = _: lib.head aspect-chain; 1385 + adaptArgs = lib.id; 1386 + }; 1387 + 1388 + den.aspects.tux = { 1389 + includes = [ gitClass ]; 1390 + git.userEmail = "root@linux.com"; 1391 + }; 1392 + ``` 1393 + 1394 + This will set at host: `home-manager.users.tux.programs.git.userEmail` 1395 + 1396 + #### Example: A `nix` class that propagates settings to NixOS and HomeManager 1397 + 1398 + This can be used when you don't want NixOS and HomeManager to share the 1399 + same pkgs but still configure both at the same time. 1400 + > Contributed by @musjj 1401 + 1402 + ```nix 1403 + nixClass = 1404 + { class, aspect-chain }: 1405 + den._.forward { 1406 + each = [ "nixos" "homeManager" ]; 1407 + fromClass = _: "nix"; 1408 + intoClass = lib.id; 1409 + intoPath = _: [ "nix" "settings" ]; 1410 + fromAspect = _: lib.head aspect-chain; 1411 + adaptArgs = lib.id; 1412 + }; 1413 + 1414 + # enable class for all users: 1415 + den.ctx.user.includes = [ nixClass ]; 1416 + 1417 + # custom aspect that uses the `nix` class. 1418 + nix-allowed = { user, ... }: { nix.allowed-users = [ user.userName ]; }; 1419 + 1420 + # included at users who can fix things with nix. 1421 + den.aspects.tux.includes = [ nix-allowed ]; 1422 + ``` 1423 + 1424 + #### Example: An impermanence class 1425 + 1426 + > Suggested by @Doc-Steve 1427 + 1428 + The following example, creates a forwarding class that is propagated only 1429 + when `environment.persistance` option is available in the host (the impermanence module was imported in host) 1430 + 1431 + One cool thing about these custom classes is that aspects can simply define 1432 + settings at the new class, without having to worry if the options they depend or 1433 + some capability is enabled. 1434 + 1435 + The froward-guard itself is reponsible checking in only one place, instead of having `mkIf` in a lot of places. 1436 + 1437 + ```nix 1438 + # Custom `persys` forwards config into nixos.environment.persistance."/nix/persist/system" 1439 + # only if environment.persistance option is present. 1440 + persys = { class, aspect-chain }: den._.forward { 1441 + each = lib.singleton true; 1442 + fromClass = _: "persys"; 1443 + intoClass = _: class; 1444 + intoPath = _: [ "environment" "persistance" "/nix/persist/system" ]; 1445 + fromAspect = _: lib.head aspect-chain; 1446 + guard = { options, ... }@osArgs: options ? environment.persistance; 1447 + }; 1448 + 1449 + den.hosts.my-laptop.includes = [ persys ]; 1450 + 1451 + # becomes nixos.environment.persistance."/nix/persist/system".hideMounts = true; 1452 + den.aspects.my-laptop.persys.hideMounts = true; 1453 + ``` 1454 + ``` 1455 + 1456 + **File:** docs/src/content/docs/guides/namespaces.mdx (L13-114) 1457 + ```text 1458 + 1459 + A **namespace** creates a scoped aspect library under `den.ful.<name>`. 1460 + Namespaces can be: 1461 + - **Local**: defined in your flake, consumed internally. 1462 + - **Exported**: exposed via `flake.denful.<name>` for other flakes to consume. 1463 + - **Imported**: merged from upstream flakes into your local `den.ful`. 1464 + 1465 + ## Creating a Namespace 1466 + 1467 + ```nix 1468 + # modules/namespace.nix 1469 + { inputs, den, ... }: { 1470 + # Create "my" namespace (not exported to flake outputs) 1471 + imports = [ (inputs.den.namespace "my" false) ]; 1472 + 1473 + # Or create and export "eg" namespace 1474 + imports = [ (inputs.den.namespace "eg" true) ]; 1475 + } 1476 + ``` 1477 + 1478 + This creates: 1479 + - `den.ful.eg` -- the namespace attrset (aspects type). 1480 + - `eg` -- a module argument alias to `den.ful.eg`. 1481 + - `flake.denful.eg` -- flake output (if exported). 1482 + 1483 + ## Populating a Namespace 1484 + 1485 + Define aspects under the namespace using any module: 1486 + 1487 + ```nix 1488 + # modules/aspects/vim.nix 1489 + { 1490 + eg.vim = { 1491 + homeManager.programs.vim.enable = true; 1492 + }; 1493 + } 1494 + ``` 1495 + 1496 + ```nix 1497 + # modules/aspects/desktop.nix 1498 + { eg, ... }: { 1499 + eg.desktop = { 1500 + includes = [ eg.vim ]; 1501 + nixos.services.xserver.enable = true; 1502 + }; 1503 + } 1504 + ``` 1505 + 1506 + ## Using Namespaced Aspects 1507 + 1508 + Reference them by their namespace: 1509 + 1510 + ```nix 1511 + { eg, ... }: { 1512 + den.aspects.laptop.includes = [ 1513 + eg.desktop 1514 + eg.vim 1515 + ]; 1516 + } 1517 + ``` 1518 + 1519 + ## Importing from Upstream 1520 + 1521 + Merge aspects from other flakes: 1522 + 1523 + ```nix 1524 + # modules/namespace.nix 1525 + { inputs, ... }: { 1526 + # Import "shared" namespace from upstream, merging with local definitions 1527 + imports = [ (inputs.den.namespace "shared" [ inputs.team-config ]) ]; 1528 + } 1529 + ``` 1530 + 1531 + The namespace function accepts: 1532 + - A **boolean** (`true`/`false`) for local/exported namespaces. 1533 + - A **list of sources** to merge from upstream flakes. 1534 + Each source's `flake.denful.<name>` is merged into `den.ful.<name>`. 1535 + 1536 + ## Enabling Angle Brackets 1537 + 1538 + When using namespaces, enable angle bracket syntax for terser references: 1539 + 1540 + ```nix 1541 + { den, ... }: { 1542 + _module.args.__findFile = den.lib.__findFile; 1543 + } 1544 + ``` 1545 + 1546 + Then reference deep aspects with `<namespace/path>`: 1547 + 1548 + ```nix 1549 + den.aspects.laptop.includes = [ <eg/desktop> ]; 1550 + ``` 1551 + 1552 + ## Architecture 1553 + 1554 + ```mermaid 1555 + flowchart LR 1556 + upstream["upstream flake"] -->|"flake.denful.shared"| merge["den.ful.shared"] 1557 + local["local modules"] -->|"shared.vim = ..."| merge 1558 + merge --> consumer["den.aspects.*.includes"] 1559 + ``` 1560 + ``` 1561 + 1562 + **File:** docs/src/content/docs/guides/angle-brackets.mdx (L17-65) 1563 + ```text 1564 + 1565 + ## Enabling 1566 + 1567 + Set `__findFile` via module args: 1568 + 1569 + ```nix 1570 + { den, ... }: { 1571 + _module.args.__findFile = den.lib.__findFile; 1572 + } 1573 + ``` 1574 + 1575 + ## Resolution Rules 1576 + 1577 + The `<name>` expression resolves through these paths in order: 1578 + 1579 + 1. **`<den.x.y>`** -- resolves to `config.den.x.y` 1580 + 2. **`<aspect>`** -- resolves to `config.den.aspects.aspect` (if `aspect` exists in `den.aspects`) 1581 + 3. **`<aspect/sub>`** -- resolves to `config.den.aspects.aspect.provides.sub` 1582 + 4. **`<namespace>`** -- resolves to `config.den.ful.namespace` (if it is a denful) 1583 + 1584 + The `/` separator is translated to `.provides.` in the lookup path. 1585 + 1586 + ## Examples 1587 + 1588 + ```nix 1589 + # Without angle brackets 1590 + den.aspects.laptop.includes = [ 1591 + den.aspects.tools.provides.editors 1592 + den.aspects.alice.provides.work-vpn 1593 + den.provides.primary-user 1594 + ]; 1595 + 1596 + # With angle brackets 1597 + den.aspects.laptop.includes = [ 1598 + <tools/editors> 1599 + <alice/work-vpn> 1600 + <den.provides.primary-user> 1601 + ]; 1602 + ``` 1603 + 1604 + ## When to Use 1605 + 1606 + Angle brackets are optional syntactic sugar. They are useful when: 1607 + - You have deeply nested provides and want shorter references. 1608 + - You are working with namespaces and want concise cross-references. 1609 + 1610 + They are functionally identical to direct attribute access. The choice 1611 + is a matter of style. 1612 + 1613 + ``` 1614 + 1615 + **File:** docs/src/content/docs/guides/declare-hosts.mdx (L17-152) 1616 + ```text 1617 + ## Host Declaration 1618 + 1619 + `den.hosts` is keyed by `<system>.<name>`: 1620 + 1621 + ```nix 1622 + { 1623 + den.hosts.x86_64-linux.laptop.users.alice = { }; 1624 + 1625 + den.hosts.aarch64-darwin.mac = { 1626 + users.alice = { }; 1627 + brew.apps = [ "iterm2" ]; 1628 + }; 1629 + } 1630 + ``` 1631 + 1632 + Each host entry produces a configuration in `flake.nixosConfigurations` or 1633 + `flake.darwinConfigurations` depending on its `class` (auto-detected from the host platform). 1634 + 1635 + ## Host Schema 1636 + 1637 + <Aside title="Important" icon="nix"> 1638 + Be sure to read about the entity [Schemas](/reference/schema/), each of `host`/`user`/`home` has a corresponding `den.schema.*` module for meta-configuration of entity features. Which can later be used in aspects that read from `host`. These schemas are the meta-data equivalent of what Dendritic flake-parts users do with flake-level options. 1639 + </Aside> 1640 + 1641 + Hosts have these options (all with sensible defaults): 1642 + 1643 + | Option | Default | Description | 1644 + |---|---|---| 1645 + | `name` | attrset key | Configuration name | 1646 + | `hostName` | `name` | Network hostname | 1647 + | `system` | parent key | `x86_64-linux`, `aarch64-darwin`, etc. | 1648 + | `class` | `"nixos"` or `"darwin"` | OS class, auto-detected from system | 1649 + | `aspect` | `name` | Primary aspect name | 1650 + | `instantiate` | class-dependent | `lib.nixosSystem`, `darwinSystem`, etc. | 1651 + | `intoAttr` | class-dependent | Flake output path | 1652 + | `users` | `{}` | User account definitions | 1653 + | `*` | from `den.schema.host` | Any option defined by base module | 1654 + | `*` | | Any other free-form attribute | 1655 + 1656 + ## User Declaration 1657 + 1658 + Users are declared as part of a host: 1659 + 1660 + ```nix 1661 + den.hosts.x86_64-linux.laptop = { 1662 + users.alice = { }; 1663 + users.bob.classes = [ "homeManager" "hjem" ]; 1664 + }; 1665 + ``` 1666 + 1667 + Each user has: 1668 + 1669 + | Option | Default | Description | 1670 + |---|---|---| 1671 + | `name` | attrset key | User configuration name | 1672 + | `userName` | `name` | System account name | 1673 + | `aspect` | `name` | Primary aspect name | 1674 + | `classes` | `[ "homeManager" ]` | Nix classes this user participates in | 1675 + | `*` | from `den.schema.user` | Any option defined by base module | 1676 + | `*` | | Any other free-form attribute | 1677 + 1678 + ## Standalone Homes 1679 + 1680 + For systems without root access or for home-manager-only setups: 1681 + 1682 + ```nix 1683 + { 1684 + den.homes.x86_64-linux.alice = { }; 1685 + den.homes.aarch64-darwin.alice = { }; 1686 + } 1687 + ``` 1688 + 1689 + Standalone homes produce `flake.homeConfigurations.<name>`. 1690 + 1691 + Home schema: 1692 + 1693 + | Option | Default | Description | 1694 + |---|---|---| 1695 + | `name` | attrset key | Home configuration name | 1696 + | `userName` | `name` | User account name | 1697 + | `system` | parent key | Platform system | 1698 + | `class` | `"homeManager"` | Home class | 1699 + | `aspect` | `name` | Primary aspect name | 1700 + | `pkgs` | `inputs.nixpkgs.legacyPackages.${system}` | nixpkgs instance | 1701 + | `instantiate` | `inputs.home-manager.lib.homeManagerConfiguration` | Builder function | 1702 + | `*` | from `den.schema.host` | Any option defined by base module | 1703 + | `*` | | Any other free-form attribute | 1704 + 1705 + ## Base Modules 1706 + 1707 + `den.schema.{host,user,home}` provides shared configuration applied to all entities of each kind. 1708 + 1709 + Some batteries also extend base modules [see `home-env.nix`](https://github.com/vic/den/blob/main/nix/home-env.nix) 1710 + that is used by home-manager/hjem/maid to extend the Host schema with options like `hjem.module`/`home-manager.module`. 1711 + 1712 + Another more advanced examples is our [templates/microvm](https://github.com/vic/den/tree/main/templates/microvm/modules/microvm-integration.nix) 1713 + that adds options related to running virtualized OS. 1714 + 1715 + ```nix 1716 + { 1717 + # Can be used to specify features of all host 1718 + den.schema.host.home-manager.enable = true; 1719 + 1720 + # Can be used to add schema options with defaults 1721 + den.schema.user = { user, lib, ... }: { 1722 + options.groupName = lib.mkOption { default = user.userName; }; 1723 + }; 1724 + 1725 + # Applied to every host and user and home. 1726 + den.schema.conf = { 1727 + options.copyright = lib.mkOption { default = "Copy-Left"; }; 1728 + }; 1729 + } 1730 + ``` 1731 + 1732 + ## Freeform Schema 1733 + 1734 + Host and user types use `freeformType`, so you can add arbitrary attributes: 1735 + 1736 + ```nix 1737 + den.hosts.x86_64-linux.laptop = { 1738 + users.alice = { }; 1739 + gpu = "nvidia"; # custom attribute, accessible in aspects via host.gpu 1740 + }; 1741 + ``` 1742 + 1743 + Access custom attributes in aspects: 1744 + 1745 + ```nix 1746 + den.aspects.laptop.includes = [ 1747 + ({ host, ... }: lib.optionalAttrs (host ? gpu) { 1748 + nixos.hardware.nvidia.enable = true; 1749 + }) 1750 + ]; 1751 + ``` 1752 + ``` 1753 + 1754 + **File:** docs/src/content/docs/guides/home-manager.mdx (L19-158) 1755 + ```text 1756 + 1757 + All Home integrations are opt-in and must be enabled explicitly. 1758 + 1759 + ```nix 1760 + # Per user 1761 + den.hosts.x86_64-linux.igloo.users.tux.classes = [ "homeManager" "hjem" ]; 1762 + 1763 + # As default for all users, unless they specify other classes. 1764 + den.schema.user.classes = lib.mkDefault [ "homeManager" ]; 1765 + ``` 1766 + 1767 + Home integration contexts, like `den.ctx.hm-host` only activate when 1768 + at least one host user has `homeManager` in their `classes`. 1769 + When true, the integration imports the HomeManager OS module, 1770 + and forwards each user's `homeManager` class into `home-manager.users.<userName>`. 1771 + 1772 + Same details regarding `home-manager` apply to other home types like `hjem` and `maid`. 1773 + 1774 + ### Requirements 1775 + 1776 + - `inputs.home-manager` must exist in your flake inputs or have custom `host.home-manager.module`. 1777 + - At least one user must have `homeManager` in their `classes`. 1778 + 1779 + ### How it Works 1780 + 1781 + ```mermaid 1782 + flowchart TD 1783 + host["den.ctx.host"] -->|"into.hm-host"| hmhost["den.ctx.hm-host"] 1784 + hmhost -->|"imports HM module"| mod["home-manager.nixosModules"] 1785 + hmhost -->|"into.hm-user (per user)"| hmuser["den.ctx.hm-user"] 1786 + hmuser -->|"forward homeManager class"| target["home-manager.users.alice"] 1787 + ``` 1788 + 1789 + 1. `hm-os.nix` detects hosts with HM-enabled users and supported OS class. 1790 + 2. It produces `den.ctx.hm-host`, which imports the HM OS-level module. 1791 + 3. `hm-integration.nix` creates `den.ctx.hm-user` per HM user, forwarding 1792 + the `homeManager` class into `home-manager.users.<userName>`. 1793 + 1794 + ### Configuring Home Manager 1795 + 1796 + ```nix 1797 + den.aspects.alice = { 1798 + homeManager = { pkgs, ... }: { 1799 + home.packages = [ pkgs.htop ]; 1800 + programs.git.enable = true; 1801 + }; 1802 + }; 1803 + ``` 1804 + 1805 + The `homeManager` class contents are forwarded to the OS-level 1806 + `home-manager.users.alice` automatically. 1807 + 1808 + ### Custom HM Module 1809 + 1810 + Override the HM module per host if needed: 1811 + 1812 + ```nix 1813 + den.hosts.x86_64-linux.laptop = { 1814 + users.vic.classes = [ "home-manager" ]; 1815 + home-manager.module = inputs.home-manager-unstable.nixosModules.home-manager; 1816 + }; 1817 + ``` 1818 + 1819 + ## Standalone Homes 1820 + 1821 + For machines without root access: 1822 + 1823 + ```nix 1824 + den.homes.x86_64-linux.alice = { }; 1825 + ``` 1826 + 1827 + This produces `flake.homeConfigurations.alice`, built with 1828 + `inputs.home-manager.lib.homeManagerConfiguration`. 1829 + 1830 + ## hjem 1831 + 1832 + [hjem](https://github.com/feel-co/hjem) is an alternative, lightweight home environment manager. 1833 + 1834 + ### Enabling 1835 + 1836 + ```nix 1837 + # Per host 1838 + den.hosts.x86_64-linux.laptop = { 1839 + users.alice.classes = [ "hjem" ]; 1840 + }; 1841 + 1842 + # On all hosts 1843 + den.schema.host.hjem.enable = true; 1844 + ``` 1845 + 1846 + ### Requirements 1847 + 1848 + - `inputs.hjem` must exist. 1849 + - Users must have `hjem` in their `classes`. 1850 + 1851 + ### Using 1852 + 1853 + ```nix 1854 + den.aspects.alice.hjem = { }; 1855 + ``` 1856 + 1857 + ## nix-maid 1858 + 1859 + [nix-maid](https://github.com/nix-maid) is another user-environment manager for NixOS. 1860 + 1861 + ### Enabling 1862 + 1863 + ```nix 1864 + den.hosts.x86_64-linux.laptop = { 1865 + users.alice.classes = [ "maid" ]; 1866 + }; 1867 + ``` 1868 + 1869 + ### Requirements 1870 + 1871 + - `inputs.nix-maid` must exist. 1872 + - Host class must be `"nixos"`. 1873 + - Users must have `maid` in their `classes`. 1874 + 1875 + ### Using 1876 + 1877 + ```nix 1878 + den.aspects.alice.maid = { 1879 + # nix-maid configuration 1880 + }; 1881 + ``` 1882 + 1883 + ## Multiple User Environments 1884 + 1885 + A user can participate in multiple environments: 1886 + 1887 + ```nix 1888 + den.hosts.x86_64-linux.laptop = { 1889 + users.alice.classes = [ "homeManager" "hjem" ]; 1890 + home-manager.enable = true; 1891 + hjem.enable = true; 1892 + }; 1893 + ``` 1894 + 1895 + Both `homeManager` and `hjem` configurations from `den.aspects.alice` will 1896 + ``` 1897 + 1898 + **File:** docs/src/content/docs/explanation/context-system.mdx (L14-95) 1899 + ```text 1900 + 1901 + A **context** is a named stage in Den's evaluation pipeline. Each context type is declared 1902 + under `den.ctx.<name>` and carries: 1903 + 1904 + - **Aspect definitions** (`provides.<name>`) -- what this context contributes to the resolved aspect. 1905 + - **Transformations** (`into.<other>`) -- functions that produce new contexts from the current one. 1906 + - **Provides** (`provides.<other>`) -- cross-context providers injected by other context definitions. 1907 + 1908 + ## Built-in Context Types 1909 + 1910 + ```mermaid 1911 + flowchart TD 1912 + host["{host}"] -->|"into.user (per user)"| user["{host, user}"] 1913 + host -->|"into.hm-host (if HM enabled)"| hmhost["{host} hm-host"] 1914 + hmhost -->|"into.hm-user (per HM user)"| hmuser["{host, user} hm-user"] 1915 + host -->|"into.wsl-host (if WSL enabled)"| wslhost["{host} wsl-host"] 1916 + host -->|"into.hjem-host (if hjem enabled)"| hjemhost["{host} hjem-host"] 1917 + hjemhost -->|"into.hjem-user"| hjemuser["{host, user}"] 1918 + host -->|"into.maid-host (if maid enabled)"| maidhost["{host} maid-host"] 1919 + maidhost -->|"into.maid-user"| maiduser["{host, user}"] 1920 + ``` 1921 + 1922 + The framework defines these contexts in `modules/context/os.nix` and the various battery modules. 1923 + Each battery (Home Manager, hjem, nix-maid, WSL) registers its own `into.*` transitions on the `host` context. 1924 + 1925 + ## Context Type Anatomy 1926 + 1927 + A context type at `den.ctx.host`: 1928 + 1929 + ```nix 1930 + den.ctx.host = { 1931 + description = "OS"; 1932 + 1933 + # The main aspect activated by this context 1934 + provides.host = { host }: den.aspects.${host.aspect}; 1935 + 1936 + # How this context contributes an aspect to other derived contexts 1937 + provides.user = { host, user }: den.aspects.other-aspect; 1938 + 1939 + # How to derive other contexts from this one 1940 + into.user = { host }: 1941 + map (user: { inherit host user; }) (lib.attrValues host.users); 1942 + }; 1943 + ``` 1944 + 1945 + The `_` (or `provides`) attrset maps context names to functions that take the current 1946 + context data and return aspect fragments. The `into` attrset maps to functions that 1947 + produce lists of new context values. 1948 + 1949 + ## Context Resolution 1950 + 1951 + When Den processes a host, it calls `den.ctx.host { host = ...; }`. This triggers: 1952 + 1953 + 1. `collectPairs` walks the context type's `into.*` transitions recursively, 1954 + building a list of `{ ctx, ctxDef, source }` pairs. 1955 + 2. `dedupIncludes` processes these pairs, applying `parametric.fixedTo` for the 1956 + first occurrence of each context type and `parametric.atLeast` for subsequent 1957 + ones, preventing duplicate owned configs. 1958 + 3. The result is a flat list of aspect fragments merged into one `deferredModule`. 1959 + 1960 + ## Custom Contexts 1961 + 1962 + You can define your own alternative context piplelines outside of `den.ctx.host` or 1963 + create custom context types for domain-specific needs like cloud infrastructure: 1964 + 1965 + ```nix 1966 + den.ctx.my-service = { 1967 + description = "Custom service context"; 1968 + provides.my-service = den.aspects.my-service; 1969 + }; 1970 + 1971 + den.ctx.host.into.my-service = { host }: 1972 + lib.optional host.my-service.enable { inherit host; }; 1973 + 1974 + den.aspects.my-service = { host }: { 1975 + nixos.services.my-service.hostName = host.hostName; 1976 + }; 1977 + 1978 + 1979 + 1980 + ``` 1981 + 1982 + ``` 1983 + 1984 + **File:** docs/src/content/docs/reference/schema.mdx (L55-163) 1985 + ```text 1986 + ## `den.schema` 1987 + 1988 + Base modules merged into all hosts, users, or homes. 1989 + 1990 + | Option | Type | Description | 1991 + |--------|------|-------------| 1992 + | `den.schema.conf` | `deferredModule` | Applied to host, user, and home | 1993 + | `den.schema.host` | `deferredModule` | Applied to all hosts (imports `conf`) | 1994 + | `den.schema.user` | `deferredModule` | Applied to all users (imports `conf`) | 1995 + | `den.schema.home` | `deferredModule` | Applied to all homes (imports `conf`) | 1996 + 1997 + ```nix 1998 + den.schema.conf = { lib, ... }: { 1999 + # shared across all host/user/home declarations 2000 + }; 2001 + den.schema.host = { ... }: { 2002 + # host-specific base config 2003 + }; 2004 + ``` 2005 + 2006 + 2007 + ## `den.hosts` 2008 + 2009 + Type: `attrsOf systemType` 2010 + 2011 + Keyed by system string (e.g., `"x86_64-linux"`). Each system contains 2012 + host definitions as freeform attribute sets. 2013 + 2014 + ```nix 2015 + den.hosts.x86_64-linux.myhost = { 2016 + users.vic = {}; 2017 + }; 2018 + ``` 2019 + 2020 + ### Host options 2021 + 2022 + | Option | Type | Default | Description | 2023 + |--------|------|---------|-------------| 2024 + | `name` | `str` | attr name | Configuration name | 2025 + | `hostName` | `str` | `name` | Network hostname | 2026 + | `system` | `str` | parent key | Platform (e.g., `x86_64-linux`) | 2027 + | `class` | `str` | auto | `"nixos"` or `"darwin"` based on system | 2028 + | `aspect` | `str` | `name` | Main aspect name for this host | 2029 + | `description` | `str` | auto | `class.hostName@system` | 2030 + | `users` | `attrsOf userType` | `{}` | User accounts on this host | 2031 + | `instantiate` | `raw` | auto | OS builder function | 2032 + | `intoAttr` | `listOf str` | auto | Flake output path | 2033 + | `*` | `den.schema.host` options | | Options from base module | 2034 + | `*` | | | free-form attributes | 2035 + 2036 + ### `instantiate` defaults 2037 + 2038 + | Class | Default | 2039 + |-------|---------| 2040 + | `nixos` | `inputs.nixpkgs.lib.nixosSystem` | 2041 + | `darwin` | `inputs.darwin.lib.darwinSystem` | 2042 + | `systemManager` | `inputs.system-manager.lib.makeSystemConfig` | 2043 + 2044 + ### `intoAttr` defaults 2045 + 2046 + | Class | Default | 2047 + |-------|---------| 2048 + | `nixos` | `[ "nixosConfigurations" name ]` | 2049 + | `darwin` | `[ "darwinConfigurations" name ]` | 2050 + | `systemManager` | `[ "systemConfigs" name ]` | 2051 + 2052 + ## `den.hosts.<sys>.<host>.users` 2053 + 2054 + Type: `attrsOf userType` 2055 + 2056 + ### User options 2057 + 2058 + | Option | Type | Default | Description | 2059 + |--------|------|---------|-------------| 2060 + | `name` | `str` | attr name | User configuration name | 2061 + | `userName` | `str` | `name` | System account name | 2062 + | `classes` | `listOf str` | `[ "homeManager" ]` | Home management classes | 2063 + | `aspect` | `str` | `name` | Main aspect name | 2064 + | `*` | `den.schema.user` options | | Options from base module | 2065 + | `*` | | | free-form attributes | 2066 + 2067 + Freeform: additional attributes pass through to the user module. 2068 + 2069 + ## `den.homes` 2070 + 2071 + Type: `attrsOf homeSystemType` 2072 + 2073 + Standalone home-manager configurations, keyed by system string. 2074 + 2075 + ```nix 2076 + den.homes.x86_64-linux.vic = {}; 2077 + ``` 2078 + 2079 + ### Home options 2080 + 2081 + | Option | Type | Default | Description | 2082 + |--------|------|---------|-------------| 2083 + | `name` | `str` | attr name | Home configuration name | 2084 + | `userName` | `str` | `name` | User account name | 2085 + | `system` | `str` | parent key | Platform system | 2086 + | `class` | `str` | `"homeManager"` | Home management class | 2087 + | `aspect` | `str` | `name` | Main aspect name | 2088 + | `description` | `str` | auto | `home.userName@system` | 2089 + | `pkgs` | `raw` | `inputs.nixpkgs.legacyPackages.$sys` | Nixpkgs instance | 2090 + | `instantiate` | `raw` | `inputs.home-manager.lib.homeManagerConfiguration` | Builder | 2091 + | `intoAttr` | `listOf str` | `[ "homeConfigurations" name ]` | Output path | 2092 + | `*` | `den.schema.home` options | | Options from base module | 2093 + | `*` | | | free-form attributes | 2094 + 2095 + ``` 2096 + 2097 + **File:** docs/src/content/docs/reference/lib.mdx (L13-134) 2098 + ```text 2099 + ## `den.lib.parametric` 2100 + 2101 + Wraps an aspect with a `__functor` that filters `includes` by argument compatibility. 2102 + 2103 + ```nix 2104 + den.lib.parametric { nixos.x = 1; includes = [ ... ]; } 2105 + ``` 2106 + 2107 + Default uses `atLeast` matching. 2108 + 2109 + ### `den.lib.parametric.atLeast` 2110 + 2111 + Same as `parametric`. Functions match if all required params are present. 2112 + 2113 + ### `den.lib.parametric.exactly` 2114 + 2115 + Functions match only if required params exactly equal provided params. 2116 + 2117 + ```nix 2118 + den.lib.parametric.exactly { includes = [ ({ host }: ...) ]; } 2119 + ``` 2120 + 2121 + ### `den.lib.parametric.fixedTo` 2122 + 2123 + Calls the aspect with a fixed context, ignoring the actual context: 2124 + 2125 + ```nix 2126 + den.lib.parametric.fixedTo { host = myHost; } someAspect 2127 + ``` 2128 + 2129 + ### `den.lib.parametric.expands` 2130 + 2131 + Extends the received context with additional attributes before dispatch: 2132 + 2133 + ```nix 2134 + den.lib.parametric.expands { extra = true; } someAspect 2135 + ``` 2136 + 2137 + ### `den.lib.parametric.withOwn` 2138 + 2139 + Low-level constructor. Takes a `functor: self -> ctx -> aspect` and wraps 2140 + an aspect so that owned configs and statics are included at the static 2141 + stage, and the functor runs at the parametric stage. 2142 + 2143 + ## `den.lib.canTake` 2144 + 2145 + Function argument introspection. 2146 + 2147 + ### `den.lib.canTake params fn` 2148 + 2149 + Returns `true` if `fn`'s required arguments are satisfied by `params` (atLeast). 2150 + 2151 + ### `den.lib.canTake.atLeast params fn` 2152 + 2153 + Same as `canTake`. 2154 + 2155 + ### `den.lib.canTake.exactly params fn` 2156 + 2157 + Returns `true` only if `fn`'s required arguments exactly match `params`. 2158 + 2159 + ## `den.lib.take` 2160 + 2161 + Conditional function application. 2162 + 2163 + ### `den.lib.take.atLeast fn ctx` 2164 + 2165 + Calls `fn ctx` if `canTake.atLeast ctx fn`, otherwise returns `{}`. 2166 + 2167 + ### `den.lib.take.exactly fn ctx` 2168 + 2169 + Calls `fn ctx` if `canTake.exactly ctx fn`, otherwise returns `{}`. 2170 + 2171 + ### `den.lib.take.unused` 2172 + 2173 + `_unused: used: used` -- ignores first argument, returns second. Used for 2174 + discarding `aspect-chain` in `import-tree`. 2175 + 2176 + ## `den.lib.statics` 2177 + 2178 + Extracts only static includes from an aspect (non-function includes): 2179 + 2180 + ```nix 2181 + den.lib.statics someAspect { class = "nixos"; aspect-chain = []; } 2182 + ``` 2183 + 2184 + ## `den.lib.owned` 2185 + 2186 + Extracts owned configs from an aspect (removes `includes`, `__functor`): 2187 + 2188 + ```nix 2189 + den.lib.owned someAspect 2190 + ``` 2191 + 2192 + ## `den.lib.isFn` 2193 + 2194 + Returns `true` if the value is a function or has `__functor`: 2195 + 2196 + ```nix 2197 + den.lib.isFn myValue 2198 + ``` 2199 + 2200 + ## `den.lib.isStatic` 2201 + 2202 + Returns `true` if the function can take `{ class, aspect-chain }`: 2203 + 2204 + ```nix 2205 + den.lib.isStatic myFn 2206 + ``` 2207 + 2208 + ## `den.lib.__findFile` 2209 + 2210 + The angle bracket resolver. See [Angle Brackets Syntax](/guides/angle-brackets/). 2211 + 2212 + ```nix 2213 + _module.args.__findFile = den.lib.__findFile; 2214 + ``` 2215 + 2216 + ## `den.lib.aspects` 2217 + 2218 + The full [flake-aspects](https://github.com/vic/flake-aspects) API, 2219 + initialized with the current `lib`. Provides `resolve`, `merge`, type 2220 + definitions, and aspect manipulation functions. 2221 + ``` 2222 + 2223 + **File:** docs/src/content/docs/guides/debug.md (L1-100) 2224 + ```markdown 2225 + --- 2226 + title: Debug Configurations 2227 + description: Tools and techniques for debugging Den configurations. 2228 + --- 2229 + 2230 + ## REPL Inspection 2231 + 2232 + Load your flake and explore interactively: 2233 + 2234 + ```console 2235 + $ nix repl 2236 + nix-repl> :lf . 2237 + nix-repl> nixosConfigurations.igloo.config.networking.hostName 2238 + "igloo" 2239 + ``` 2240 + 2241 + ## Expose `den` for Inspection 2242 + 2243 + Temporarily expose the `den` attrset as a flake output: 2244 + 2245 + ```nix 2246 + { den, ... }: { 2247 + flake.den = den; # remove after debugging 2248 + } 2249 + ``` 2250 + 2251 + Then in REPL: 2252 + 2253 + ```console 2254 + nix-repl> :lf . 2255 + nix-repl> den.aspects.igloo 2256 + nix-repl> den.hosts.x86_64-linux.igloo 2257 + nix-repl> den.ctx 2258 + ``` 2259 + 2260 + ## Trace Context 2261 + 2262 + Print context values during evaluation: 2263 + 2264 + ```nix 2265 + den.aspects.laptop.includes = [ 2266 + ({ host, ... }@ctx: builtins.trace ctx { 2267 + nixos.networking.hostName = host.hostName; 2268 + }) 2269 + ]; 2270 + ``` 2271 + 2272 + ## Break into REPL 2273 + 2274 + Drop into a REPL at any evaluation point: 2275 + 2276 + ```nix 2277 + den.aspects.laptop.includes = [ 2278 + ({ host, ... }@ctx: builtins.break ctx { 2279 + nixos = { }; 2280 + }) 2281 + ]; 2282 + ``` 2283 + 2284 + ## Manually Resolve an Aspect 2285 + 2286 + Test how an aspect resolves for a specific class: 2287 + 2288 + ```console 2289 + nix-repl> module = den.lib.aspects.resolve "nixos" [] den.aspects.laptop; 2290 + nix-repl> config = (lib.evalModules { modules = [ module ]; }).config 2291 + ``` 2292 + 2293 + For parametric aspects, apply context first: 2294 + 2295 + ```console 2296 + nix-repl> aspect = den.aspects.laptop { host = den.hosts.x86_64-linux.laptop; } 2297 + nix-repl> module = den.lib.aspects.resolve "nixos" [] aspect; 2298 + ``` 2299 + 2300 + ## Inspect a Host's Main Module 2301 + 2302 + ```console 2303 + nix-repl> module = den.hosts.x86_64-linux.igloo.mainModule 2304 + nix-repl> cfg = (lib.nixosSystem { modules = [ module ]; }).config 2305 + nix-repl> cfg.networking.hostName 2306 + ``` 2307 + 2308 + ## Common Issues 2309 + 2310 + **Duplicate values in lists**: Den deduplicates owned and static configs 2311 + from `den.default`, but parametric functions in `den.default.includes` 2312 + run at every context stage. Use `den.lib.take.exactly` to restrict: 2313 + 2314 + ```nix 2315 + den.lib.take.exactly ({ host }: { nixos.x = 1; }) 2316 + ``` 2317 + 2318 + **Missing attribute**: The context does not have the expected parameter. 2319 + Trace context keys to see what is available. 2320 + 2321 + **Wrong class**: Check that `host.class` matches what you expect. 2322 + Darwin hosts have `class = "darwin"`, not `"nixos"`. 2323 + 2324 + **Module not found**: Ensure the file is under `modules/` and not 2325 + ``` 2326 + 2327 + **File:** docs/src/content/docs/tutorials/ci.md (L1-164) 2328 + ```markdown 2329 + --- 2330 + title: "Template: CI Tests" 2331 + description: Den's own test suite — the definitive reference for every feature. 2332 + --- 2333 + 2334 + The CI template is Den's comprehensive test suite. It tests every feature using [nix-unit](https://github.com/nix-community/nix-unit). This is the **best learning resource** for understanding exactly how Den behaves. 2335 + 2336 + ## Structure 2337 + 2338 + ``` 2339 + flake.nix 2340 + modules/ 2341 + empty.nix # example test skeleton 2342 + test-support/ 2343 + eval-den.nix # denTest + evalDen helpers 2344 + nix-unit.nix # nix-unit integration 2345 + features/ 2346 + angle-brackets.nix # <den/...> syntax 2347 + conditional-config.nix # conditional imports/configs 2348 + default-includes.nix # den.default behavior 2349 + forward.nix # den._.forward 2350 + homes.nix # standalone HM 2351 + host-options.nix # host/user schema options 2352 + namespaces.nix # namespace define/merge/export 2353 + os-user-class.nix # user class forwarding 2354 + parametric.nix # parametric functors 2355 + schema-base-modules.nix # den.schema modules 2356 + special-args-custom-instantiate.nix # custom instantiation 2357 + top-level-parametric.nix # top-level context aspects 2358 + user-host-bidirectional-config.nix # bidirectional providers 2359 + batteries/ 2360 + define-user.nix # define-user battery 2361 + flake-parts.nix # inputs' and self' 2362 + import-tree.nix # import-tree battery 2363 + primary-user.nix # primary-user battery 2364 + tty-autologin.nix # tty-autologin battery 2365 + unfree.nix # unfree packages 2366 + user-shell.nix # user-shell battery 2367 + context/ 2368 + apply.nix # ctx application 2369 + apply-non-exact.nix # non-exact matching 2370 + cross-provider.nix # cross-provider mechanism 2371 + custom-ctx.nix # custom context types 2372 + den-default.nix # den.default as context 2373 + host-propagation.nix # full host pipeline 2374 + named-provider.nix # self-named providers 2375 + deadbugs/ 2376 + _external-namespace-deep-aspect.nix 2377 + static-include-dup-package.nix 2378 + home-manager/ 2379 + home-managed-home.nix 2380 + use-global-pkgs.nix 2381 + non-dendritic/ # non-den files for import-tree tests 2382 + provider/ # external namespace provider flake 2383 + ``` 2384 + 2385 + ## Test Categories 2386 + 2387 + ### Core Features 2388 + 2389 + | Test File | What It Tests | 2390 + |-----------|---------------| 2391 + | [conditional-config.nix](https://github.com/vic/den/blob/main/templates/ci/modules/features/conditional-config.nix) | Conditional imports using host/user attributes | 2392 + | [default-includes.nix](https://github.com/vic/den/blob/main/templates/ci/modules/features/default-includes.nix) | `den.default` applying to all hosts/users | 2393 + | [host-options.nix](https://github.com/vic/den/blob/main/templates/ci/modules/features/host-options.nix) | Custom host attributes, hostName, aspect names | 2394 + | [top-level-parametric.nix](https://github.com/vic/den/blob/main/templates/ci/modules/features/top-level-parametric.nix) | Context-aware top-level aspects | 2395 + | [parametric.nix](https://github.com/vic/den/blob/main/templates/ci/modules/features/parametric.nix) | All parametric functors (atLeast, fixedTo, expands) | 2396 + 2397 + ### Bidirectional & Providers 2398 + 2399 + | Test File | What It Tests | 2400 + |-----------|---------------| 2401 + | [user-host-bidirectional-config.nix](https://github.com/vic/den/blob/main/templates/ci/modules/features/user-host-bidirectional-config.nix) | Host→user and user→host config flow | 2402 + | [context/cross-provider.nix](https://github.com/vic/den/blob/main/templates/ci/modules/features/context/cross-provider.nix) | Source providing config to target context | 2403 + | [context/named-provider.nix](https://github.com/vic/den/blob/main/templates/ci/modules/features/context/named-provider.nix) | Self-named provider mechanism | 2404 + 2405 + ### Context System 2406 + 2407 + | Test File | What It Tests | 2408 + |-----------|---------------| 2409 + | [context/apply.nix](https://github.com/vic/den/blob/main/templates/ci/modules/features/context/apply.nix) | Context application mechanics | 2410 + | [context/apply-non-exact.nix](https://github.com/vic/den/blob/main/templates/ci/modules/features/context/apply-non-exact.nix) | Non-exact context matching | 2411 + | [context/custom-ctx.nix](https://github.com/vic/den/blob/main/templates/ci/modules/features/context/custom-ctx.nix) | User-defined context types with `into` | 2412 + | [context/den-default.nix](https://github.com/vic/den/blob/main/templates/ci/modules/features/context/den-default.nix) | `den.default` as a context type | 2413 + | [context/host-propagation.nix](https://github.com/vic/den/blob/main/templates/ci/modules/features/context/host-propagation.nix) | Full host pipeline with all contributions | 2414 + 2415 + ### Batteries 2416 + 2417 + | Test File | What It Tests | 2418 + |-----------|---------------| 2419 + | [batteries/define-user.nix](https://github.com/vic/den/blob/main/templates/ci/modules/features/batteries/define-user.nix) | User definition across contexts | 2420 + | [batteries/primary-user.nix](https://github.com/vic/den/blob/main/templates/ci/modules/features/batteries/primary-user.nix) | Primary user groups | 2421 + | [batteries/user-shell.nix](https://github.com/vic/den/blob/main/templates/ci/modules/features/batteries/user-shell.nix) | Shell configuration | 2422 + | [batteries/unfree.nix](https://github.com/vic/den/blob/main/templates/ci/modules/features/batteries/unfree.nix) | Unfree package predicates | 2423 + | [batteries/tty-autologin.nix](https://github.com/vic/den/blob/main/templates/ci/modules/features/batteries/tty-autologin.nix) | TTY autologin service | 2424 + | [batteries/import-tree.nix](https://github.com/vic/den/blob/main/templates/ci/modules/features/batteries/import-tree.nix) | Auto-importing class dirs | 2425 + | [batteries/flake-parts.nix](https://github.com/vic/den/blob/main/templates/ci/modules/features/batteries/flake-parts.nix) | `inputs'` and `self'` providers | 2426 + 2427 + ### Advanced 2428 + 2429 + | Test File | What It Tests | 2430 + |-----------|---------------| 2431 + | [angle-brackets.nix](https://github.com/vic/den/blob/main/templates/ci/modules/features/angle-brackets.nix) | All bracket resolution paths | 2432 + | [namespaces.nix](https://github.com/vic/den/blob/main/templates/ci/modules/features/namespaces.nix) | Local, remote, merged namespaces | 2433 + | [forward-from-custom-class.nix](https://github.com/vic/den/blob/main/templates/ci/modules/features/forward-from-custom-class.nix) | Custom class forwarding | 2434 + | [homes.nix](https://github.com/vic/den/blob/main/templates/ci/modules/features/homes.nix) | Standalone Home-Manager configs | 2435 + | [schema-base-modules.nix](https://github.com/vic/den/blob/main/templates/ci/modules/features/schema-base-modules.nix) | `den.schema.{host,user,home,conf}` | 2436 + 2437 + ### Bug Regressions 2438 + 2439 + | Test File | What It Tests | 2440 + |-----------|---------------| 2441 + | [deadbugs/static-include-dup-package.nix](https://github.com/vic/den/blob/main/templates/ci/modules/features/deadbugs/static-include-dup-package.nix) | Duplicate deduplication for packages/lists | 2442 + | [deadbugs/_external-namespace-deep-aspect.nix](https://github.com/vic/den/blob/main/templates/ci/modules/features/deadbugs/_external-namespace-deep-aspect.nix) | Deep aspect access from external flakes | 2443 + 2444 + ### External Provider 2445 + 2446 + The `provider/` subdirectory is a **separate flake** that defines a namespace `provider` with aspects. It's used by the deadbugs test to verify cross-flake namespace consumption: 2447 + 2448 + ```nix 2449 + # provider/modules/den.nix 2450 + { inputs, ... }: 2451 + { 2452 + imports = [ inputs.den.flakeModule (inputs.den.namespace "provider" true) ]; 2453 + provider.tools._.dev._.editors = { 2454 + nixos.programs.vim.enable = true; 2455 + }; 2456 + } 2457 + ``` 2458 + 2459 + ## Running CI Tests 2460 + 2461 + From the Den root against your local checkout: 2462 + 2463 + ```console 2464 + nix flake check --override-input den . ./templates/ci 2465 + ``` 2466 + 2467 + You can also run a single or a subset of tests using: 2468 + 2469 + ```console 2470 + # You can use any attr-path bellow flake.tests after system-agnositc to run those specific tests: 2471 + nix-unit --override-input den . --flake ./templates/ci#.tests.systems.x86_64-linux.system-agnostic 2472 + ``` 2473 + 2474 + ## Writing New Tests 2475 + 2476 + Copy `modules/empty.nix` as a starting point: 2477 + 2478 + ```nix 2479 + { denTest, ... }: 2480 + { 2481 + flake.tests.my-feature = { 2482 + test-name = denTest ( 2483 + { den, igloo, ... }: 2484 + { 2485 + den.hosts.x86_64-linux.igloo.users.tux = { }; 2486 + expr = /* what you get */; 2487 + expected = /* what you expect */; 2488 + } 2489 + ); 2490 + }; 2491 + } 2492 + ``` 2493 + ``` 2494 + 2495 + **File:** docs/src/content/docs/guides/migrate.mdx (L9-119) 2496 + ```text 2497 + 2498 + Migration to Den is incremental. You do not need to rewrite everything at once. 2499 + 2500 + <Steps> 2501 + 2502 + 1. Add Den as Input 2503 + 2504 + Add Den to your flake and import the flake module: 2505 + 2506 + ```nix 2507 + { 2508 + inputs.den.url = "github:vic/den"; 2509 + inputs.import-tree.url = "github:vic/import-tree"; 2510 + inputs.flake-aspects.url = "github:vic/flake-aspects"; 2511 + 2512 + outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } 2513 + (inputs.import-tree ./modules); 2514 + } 2515 + ``` 2516 + 2517 + ```nix 2518 + # modules/den.nix 2519 + { inputs, ... }: { 2520 + imports = [ inputs.den.flakeModule ]; 2521 + } 2522 + ``` 2523 + 2524 + 2. Declare Hosts 2525 + 2526 + Move your host declarations into `den.hosts`: 2527 + 2528 + ```nix 2529 + { 2530 + den.hosts.x86_64-linux.laptop.users.alice = { }; 2531 + } 2532 + ``` 2533 + 2534 + 3. Import Existing Modules 2535 + 2536 + Use `den.provides.import-tree` to load your existing non-dendritic modules: 2537 + 2538 + ```nix 2539 + # modules/legacy.nix 2540 + { den, ... }: { 2541 + den.ctx.host.includes = [ 2542 + (den.provides.import-tree._.host ./hosts) 2543 + ]; 2544 + den.ctx.user.includes = [ 2545 + (den.provides.import-tree._.user ./users) 2546 + ]; 2547 + } 2548 + ``` 2549 + 2550 + With this directory structure: 2551 + 2552 + ``` 2553 + hosts/ 2554 + laptop/ 2555 + _nixos/ 2556 + hardware.nix 2557 + networking.nix 2558 + _homeManager/ 2559 + shell.nix 2560 + users/ 2561 + alice/ 2562 + _homeManager/ 2563 + git.nix 2564 + _nixos/ 2565 + groups.nix 2566 + ``` 2567 + 2568 + Files under `_nixos/` are imported as NixOS modules, `_homeManager/` as 2569 + Home Manager modules, etc. This requires `inputs.import-tree`. 2570 + 2571 + 4. Extract Aspects 2572 + 2573 + Gradually extract features from your legacy modules into Den aspects: 2574 + 2575 + ```nix 2576 + # modules/dev-tools.nix 2577 + { 2578 + den.aspects.dev-tools = { 2579 + nixos = { pkgs, ... }: { 2580 + environment.systemPackages = with pkgs; [ git vim tmux ]; 2581 + }; 2582 + homeManager.programs.git.enable = true; 2583 + }; 2584 + } 2585 + ``` 2586 + 2587 + ```nix 2588 + # modules/laptop.nix 2589 + { den, ... }: { 2590 + den.aspects.laptop.includes = [ den.aspects.dev-tools ]; 2591 + } 2592 + ``` 2593 + 2594 + 5. Remove Legacy 2595 + 2596 + As aspects replace legacy modules, remove the corresponding files from 2597 + `hosts/` and `users/`. Eventually remove `den.provides.import-tree` usage. 2598 + 2599 + </Steps> 2600 + 2601 + ## Tips 2602 + 2603 + - **Start with one host.** Migrate a single machine first to learn the pattern. 2604 + - **Keep legacy working.** `import-tree` loads your existing files alongside 2605 + Den aspects -- they coexist without conflicts. 2606 + - **Use batteries.** Replace manual user/shell/HM setup with `den.provides.*`. 2607 + - **Test with VM.** Use `nix run .#vm` to validate changes before applying to hardware. 2608 + ``` 2609 + 2610 + **File:** docs/src/content/docs/reference/ctx.mdx (L63-142) 2611 + ```text 2612 + ## Built-in Context Types 2613 + 2614 + ### `den.ctx.host` 2615 + 2616 + Context data: `{ host }` 2617 + 2618 + Produced for each `den.hosts.<system>.<name>` entry. 2619 + 2620 + Providers: 2621 + - `_.host` -- `fixedTo { host }` on the host's aspect. 2622 + - `_.user` -- `atLeast` on the host's aspect with `{ host, user }`. 2623 + 2624 + Transitions: 2625 + - `into.default` -- identity (for default aspect). 2626 + - `into.user` -- one `{ host, user }` per `host.users` entry. 2627 + - `into.hm-host` -- (from `hm-os.nix`) if HM enabled and has HM users. 2628 + - `into.wsl-host` -- (from `wsl.nix`) if WSL enabled on NixOS host. 2629 + - `into.hjem-host` -- (from `hjem-os.nix`) if hjem enabled. 2630 + - `into.maid-host` -- (from `maid-os.nix`) if nix-maid enabled. 2631 + 2632 + ### `den.ctx.user` 2633 + 2634 + Context data: `{ host, user }` 2635 + 2636 + Providers: 2637 + - `_.user` -- `fixedTo { host, user }` on the user's aspect. 2638 + 2639 + Transitions: 2640 + - `into.default` -- identity. 2641 + 2642 + ### `den.ctx.home` 2643 + 2644 + Context data: `{ home }` 2645 + 2646 + Produced for each `den.homes.<system>.<name>` entry. 2647 + 2648 + Providers: 2649 + - `_.home` -- `fixedTo { home }` on the home's aspect. 2650 + 2651 + ### `den.ctx.hm-host` 2652 + 2653 + Context data: `{ host }` 2654 + 2655 + Providers: 2656 + - `provides.hm-host` -- imports HM OS module. 2657 + 2658 + Transitions: 2659 + - `into.hm-user` -- per HM-class user. 2660 + 2661 + ### `den.ctx.hm-user` 2662 + 2663 + Context data: `{ host, user }` 2664 + 2665 + Providers: 2666 + - `_.hm-user` -- forwards `homeManager` class to `home-manager.users.<userName>`. 2667 + 2668 + ### `den.ctx.wsl-host` 2669 + 2670 + Context data: `{ host }` 2671 + 2672 + Providers: 2673 + - `provides.wsl-host` -- imports WSL module, creates `wsl` class forward. 2674 + 2675 + ## Custom Context Types 2676 + 2677 + Define new contexts to extend the pipeline: 2678 + 2679 + ```nix 2680 + { 2681 + den.ctx.gpu = { 2682 + description = "GPU-enabled host"; 2683 + _.gpu = { host }: { 2684 + nixos.hardware.nvidia.enable = true; 2685 + }; 2686 + }; 2687 + 2688 + den.ctx.host.into.gpu = { host }: 2689 + lib.optional (host ? gpu) { inherit host; }; 2690 + } 2691 + ``` 2692 + ``` 2693 + 2694 + **File:** docs/src/content/docs/explanation/context-pipeline.mdx (L15-99) 2695 + ```text 2696 + 2697 + When Den evaluates a host, it walks a pipeline of context transformations: 2698 + 2699 + ```mermaid 2700 + flowchart TD 2701 + start["den.hosts.x86_64-linux.laptop"] --> host["den.ctx.host {host}"] 2702 + host -->|"_.host"| owned["Owned config: fixedTo {host} aspects.laptop"] 2703 + host -->|"_.user"| userctx["atLeast aspects.laptop {host, user}"] 2704 + host -->|"into.user"| user["den.ctx.user {host, user} (per user)"] 2705 + user -->|"_.user"| userown["fixedTo {host,user} aspects.alice"] 2706 + host -->|"into.hm-host"| hmhost["den.ctx.hm-host {host}"] 2707 + hmhost -->|"import HM module"| hmmod["home-manager OS module"] 2708 + hmhost -->|"into.hm-user"| hmuser["den.ctx.hm-user {host, user}"] 2709 + hmuser -->|"forward homeManager class"| fwd["home-manager.users.alice"] 2710 + ``` 2711 + 2712 + <Steps> 2713 + 1. Host Context 2714 + 2715 + For each entry in `den.hosts.<system>.<name>`, Den creates a `{host}` context. 2716 + The host context type (`den.ctx.host`) contributes: 2717 + 2718 + - `_.host` -- Applies the host's own aspect with `fixedTo { host }`, making 2719 + all owned configs available for the host's class. 2720 + - `_.user` -- For each user, applies the host's aspect with `atLeast { host, user }`, 2721 + activating parametric includes that need both host and user. 2722 + 2723 + 2. User Context 2724 + 2725 + `into.user` maps each `host.users` entry into a `{host, user}` context. 2726 + The user context type (`den.ctx.user`) contributes: 2727 + 2728 + - `_.user` -- Applies the user's own aspect with `fixedTo { host, user }`. 2729 + 2730 + 3. Derived Contexts 2731 + 2732 + Batteries register additional `into.*` transformations on the host context: 2733 + 2734 + | Transition | Condition | Produces | 2735 + |---|---|---| 2736 + | `into.hm-host` | `host.home-manager.enable && hasHmUsers` | `{host}` hm-host | 2737 + | `into.hm-user` | Per HM-class user on hm-host | `{host, user}` hm-user | 2738 + | `into.wsl-host` | `host.class == "nixos" && host.wsl.enable` | `{host}` wsl-host | 2739 + | `into.hjem-host` | `host.hjem.enable && hasHjemUsers` | `{host}` hjem-host | 2740 + | `into.hjem-user` | Per hjem-class user | `{host, user}` hjem-user | 2741 + | `into.maid-host` | `host.nix-maid.enable && hasMaidUsers` | `{host}` maid-host | 2742 + | `into.maid-user` | Per maid-class user | `{host, user}` maid-user | 2743 + 2744 + Each derived context can contribute its own aspect definitions and import 2745 + the necessary OS-level modules (e.g., `home-manager.nixosModules.home-manager`). 2746 + 2747 + 3. Deduplication 2748 + 2749 + `dedupIncludes` in `modules/context/types.nix` ensures: 2750 + 2751 + - **First occurrence** of a context type uses `parametric.fixedTo`, which includes 2752 + owned configs + statics + parametric matches. 2753 + - **Subsequent occurrences** use `parametric.atLeast`, which only includes 2754 + parametric matches (owned/statics already applied). 2755 + 2756 + This prevents `den.default` configs from being applied twice when the same 2757 + aspect appears at multiple pipeline stages. 2758 + 2759 + 4. Home Configurations 2760 + 2761 + Standalone `den.homes` entries go through a separate path: 2762 + 2763 + ```mermaid 2764 + flowchart TD 2765 + home["den.homes.x86_64-linux.alice"] --> homectx["den.ctx.home {home}"] 2766 + homectx --> resolve["fixedTo {home} aspects.alice"] 2767 + resolve --> hmc["homeConfigurations.alice"] 2768 + ``` 2769 + 2770 + Home contexts have no host, so functions requiring `{ host }` are not activated. 2771 + Functions requiring `{ home }` run instead. 2772 + 2773 + 5. Output 2774 + 2775 + `modules/config.nix` collects all hosts and homes, calls `host.instantiate` 2776 + (defaults to `lib.nixosSystem`, `darwinSystem`, or `homeManagerConfiguration` 2777 + depending on class), and places results into `flake.nixosConfigurations`, 2778 + `flake.darwinConfigurations`, or `flake.homeConfigurations`. 2779 + 2780 + </Steps> 2781 + ``` 2782 + 2783 + **File:** docs/src/content/docs/tutorials/overview.md (L38-60) 2784 + ```markdown 2785 + ## Project Structure 2786 + 2787 + Every template follows the same pattern: 2788 + 2789 + ``` 2790 + flake.nix # or default.nix for noflake 2791 + modules/ 2792 + den.nix # host/user declarations + den.flakeModule import 2793 + *.nix # aspect definitions, one concern per file 2794 + ``` 2795 + 2796 + Den uses [import-tree](https://github.com/vic/import-tree) to recursively load all `.nix` files under `modules/`. You never need to manually list imports — just create files. 2797 + 2798 + ## What Each Template Demonstrates 2799 + 2800 + - **minimal** — The absolute minimum: one host, one user, no extra dependencies 2801 + - **default** — Production-ready structure with Home-Manager, VM testing, dendritic flake-file 2802 + - **example** — Namespaces, angle brackets, cross-platform (NixOS + Darwin), providers 2803 + - **noflake** — Using Den with npins instead of flakes 2804 + - **microvm** — Demostrates Den extensibility showcasing MicroVM virtualization. 2805 + - **bogus** — Creating minimal reproductions for bug reports with nix-unit 2806 + - **ci** — Comprehensive tests covering every Den feature (your best learning resource) 2807 + 2808 + ``` 2809 + 2810 + **File:** docs/src/content/docs/reference/aspects.mdx (L59-106) 2811 + ```text 2812 + ## Aspect structure 2813 + 2814 + An aspect is an attribute set with: 2815 + 2816 + | Key | Purpose | 2817 + |-----|---------| 2818 + | `<class>` | Config merged into hosts/homes of that class | 2819 + | `includes` | List of modules or functions dispatched by context | 2820 + | `__functor` | Auto-generated by `parametric`; drives dispatch | 2821 + 2822 + ### Static vs parametric includes 2823 + 2824 + Functions in `includes` receiving `{ class, aspect-chain }` are **static** -- 2825 + evaluated once during aspect resolution. Functions receiving context 2826 + arguments (`{ host }`, `{ user }`, etc.) are **parametric** -- evaluated 2827 + per context during `ctxApply`. 2828 + 2829 + ## `den.provides` 2830 + 2831 + Type: freeform `attrsOf providerType` (aliased as `den._`) 2832 + 2833 + Batteries-included reusable aspects. Each provider is a `providerType` 2834 + from `flake-aspects`. See [Batteries Reference](/reference/batteries/). 2835 + 2836 + ```nix 2837 + den._ = { 2838 + my-battery = { 2839 + nixos.services.something.enable = true; 2840 + includes = [ ./my-module.nix ]; 2841 + }; 2842 + }; 2843 + ``` 2844 + 2845 + ## Class resolution 2846 + 2847 + When aspects are resolved for a host, Den: 2848 + 2849 + 1. Collects all aspects referenced by the host 2850 + 2. Extracts the class-specific config (e.g., `nixos` for NixOS hosts) 2851 + 3. Evaluates static includes with `{ class, aspect-chain }` 2852 + 4. Builds context pairs from `den.ctx` 2853 + 5. Applies parametric includes via `ctxApply` 2854 + 6. Merges everything into the host's `evalModules` call 2855 + ``` 2856 + 2857 + **File:** README.md (L113-237) 2858 + ```markdown 2859 + ## Code example (OS configuration domain) 2860 + 2861 + ### Defining hosts, users and homes. 2862 + 2863 + ```nix 2864 + den.hosts.x86_64-linux.lap.users.vic = {}; 2865 + den.hosts.aarch64-darwin.mac.users.vic = {}; 2866 + den.homes.aarch64-darwin.vic = {}; 2867 + ``` 2868 + 2869 + ```console 2870 + $ nixos-rebuild switch --flake .#lap 2871 + $ darwin-rebuild switch --flake .#mac 2872 + $ home-manager switch --flake .#vic 2873 + ``` 2874 + 2875 + ### Extensible Schemas for hosts, users and homes. 2876 + 2877 + ```nix 2878 + # extensible base modules for common, typed schemas 2879 + den.schema.user = { user, lib, ... }: { 2880 + config.classes = 2881 + if user.userName == "vic" then [ "hjem" "maid" ] 2882 + else lib.mkDefault [ "homeManager" ]; 2883 + 2884 + options.mainGroup = lib.mkOption { default = user.userName; }; 2885 + }; 2886 + ``` 2887 + 2888 + ### Dendritic Multi-Platform Hosts 2889 + 2890 + ```nix 2891 + # modules/my-laptop.nix 2892 + { den, inputs, ... }: { 2893 + den.aspects.my-laptop = { 2894 + # re-usable configuration aspects. Den batteries and yours. 2895 + includes = [ den.provides.hostname den.aspects.work-vpn ]; 2896 + 2897 + # regular nixos/darwin modules or any other Nix class 2898 + nixos = { pkgs, ... }: { imports = [ inputs.disko.nixosModules.disko ]; }; 2899 + darwin = { pkgs, ... }: { imports = [ inputs.nix-homebrew.darwinModules.nix-homebrew ]; }; 2900 + 2901 + # Custom Nix classes. `os` applies to both nixos and darwin. contributed by @Risa-G. 2902 + # See https://den.oeiuwq.com/guides/custom-classes/#user-contributed-examples 2903 + os = { pkgs, ... }: { 2904 + environment.systemPackages = [ pkgs.direnv ]; 2905 + }; 2906 + 2907 + # host can contribute default home environments to all its users. 2908 + homeManager.programs.vim.enable = true; 2909 + }; 2910 + } 2911 + ``` 2912 + 2913 + ### Multiple User Home Environments 2914 + 2915 + ```nix 2916 + # modules/vic.nix 2917 + { den, ... }: { 2918 + den.aspects.vic = { 2919 + # supports multiple home environments, eg: for migrating from homeManager. 2920 + homeManager = { pkgs, ... }: { }; 2921 + hjem.files.".envrc".text = "use flake ~/hk/home"; 2922 + maid.kconfig.settings.kwinrc.Desktops.Number = 3; 2923 + 2924 + # user can contribute OS-configurations to any host it lives on 2925 + darwin.services.karabiner-elements.enable = true; 2926 + 2927 + # user class forwards into {nixos/darwin}.users.users.<userName> 2928 + user = { pkgs, ... }: { 2929 + packages = [ pkgs.helix ]; 2930 + description = "oeiuwq"; 2931 + }; 2932 + 2933 + includes = [ 2934 + den.provides.primary-user # re-usable batteries 2935 + (den.provides.user-shell "fish") # parametric aspects 2936 + den.aspects.tiling-wm # your own aspects 2937 + den.aspects.gaming.provides.emulators 2938 + ]; 2939 + }; 2940 + } 2941 + ``` 2942 + 2943 + ### Custom Dendritic Nix Classes 2944 + 2945 + [Custom classes](https://den.oeiuwq.com/guides/custom-classes) is how Den implements `homeManager`, `hjem`, `wsl`, `microvm` support. You can use the very same mechanism to create your own classes. 2946 + 2947 + ```nix 2948 + # Example: A class for role-based configuration between users and hosts 2949 + 2950 + roleClass = 2951 + { host, user }: 2952 + { class, aspect-chain }: 2953 + den._.forward { 2954 + each = lib.intersectLists (host.roles or []) (user.roles or []); 2955 + fromClass = lib.id; 2956 + intoClass = _: host.class; 2957 + intoPath = _: [ ]; 2958 + fromAspect = _: lib.head aspect-chain; 2959 + }; 2960 + 2961 + den.ctx.user.includes = [ roleClass ]; 2962 + 2963 + den.hosts.x86_64-linux.igloo = { 2964 + roles = [ "devops" "gaming" ]; 2965 + users = { 2966 + alice.roles = [ "gaming" ]; 2967 + bob.roles = [ "devops" ]; 2968 + }; 2969 + }; 2970 + 2971 + den.aspects.alice = { 2972 + # enabled when both support gaming role 2973 + gaming = { pkgs, ... }: { programs.steam.enable = true; }; 2974 + }; 2975 + 2976 + den.aspects.bob = { 2977 + # enabled when both support devops role 2978 + devops = { pkgs, ... }: { virtualisation.podman.enable = true; }; 2979 + 2980 + # not enabled at igloo host (bob missing gaming role on that host) 2981 + gaming = {}; 2982 + }; 2983 + ```