feat: add nix flake

Kieran Klukas created

has a home-manager module and nix module and supports configuring
through nix as well as having auto shell completions, and a man page

Change summary

README.md       |  79 +++++++++++++++
flake.lock      |  27 +++++
flake.nix       | 138 +++++++++++++++++++++++++++
nix/options.nix | 260 +++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 504 insertions(+)

Detailed changes

README.md 🔗

@@ -27,6 +27,85 @@ go install
 
 Note that Crush doesn't support Windows yet, however Windows support is planned and in progress.
 
+### Nix
+
+Crush provides a Nix flake for easy installation and configuration management.
+
+#### Installation
+
+Install directly from the flake:
+
+```bash
+nix profile install github:charmbracelet/crush
+```
+
+Or run without installing:
+
+```bash
+nix run github:charmbracelet/crush
+```
+#### NixOS Module
+
+Add Crush to your NixOS configuration:
+
+```nix
+{
+  inputs = {
+    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+    crush.url = "github:charmbracelet/crush";
+  };
+
+  outputs = { nixpkgs, crush, ... }: {
+    nixosConfigurations.your-hostname = nixpkgs.lib.nixosSystem {
+      modules = [
+        crush.nixosModules.default
+        {
+          programs.crush = {
+            enable = true;
+            settings = {
+              providers = {
+                openai = {
+                  name = "OpenAI";
+                  provider_type = "openai";
+                  api_key = "sk-fake123456789abcdef...";
+                };
+              };
+              lsp = {
+                go = { command = "gopls"; };
+                nix = { command = "nil"; };
+              };
+            };
+          };
+        }
+      ];
+    };
+  };
+}
+```
+
+#### Home Manager Module
+
+Home Manager configuration uses identical settings structure:
+
+```nix
+{
+  inputs = {
+    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+    home-manager.url = "github:nix-community/home-manager";
+    crush.url = "github:charmbracelet/crush";
+  };
+
+  outputs = { nixpkgs, home-manager, crush, ... }: {
+    homeConfigurations.your-username = home-manager.lib.homeManagerConfiguration {
+      modules = [
+        crush.homeManagerModules.default
+        { programs.crush.enable = true; }
+      ];
+    };
+  };
+}
+```
+
 ## Getting Started
 
 For now, the quickest way to get started is to set an environment variable for

flake.lock 🔗

@@ -0,0 +1,27 @@
+{
+  "nodes": {
+    "nixpkgs": {
+      "locked": {
+        "lastModified": 1752480373,
+        "narHash": "sha256-JHQbm+OcGp32wAsXTE/FLYGNpb+4GLi5oTvCxwSoBOA=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "62e0f05ede1da0d54515d4ea8ce9c733f12d9f08",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "ref": "nixos-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "root": {
+      "inputs": {
+        "nixpkgs": "nixpkgs"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}

flake.nix 🔗

@@ -0,0 +1,138 @@
+{
+  description = "Crush is a tool for building software with AI";
+
+  inputs = {
+    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+  };
+
+  outputs =
+    { self, nixpkgs, ... }:
+    let
+      allSystems = [
+        "x86_64-linux" # 64-bit Intel/AMD Linux
+        "aarch64-linux" # 64-bit ARM Linux
+        "x86_64-darwin" # 64-bit Intel macOS
+        "aarch64-darwin" # 64-bit ARM macOS
+      ];
+      forAllSystems =
+        f:
+        nixpkgs.lib.genAttrs allSystems (
+          system:
+          f {
+            pkgs = import nixpkgs { inherit system; };
+          }
+        );
+    in
+    {
+      nixosModules.default =
+        {
+          config,
+          lib,
+          pkgs,
+          ...
+        }:
+        let
+          crushOptions = import ./nix/options.nix { inherit lib; };
+        in
+        {
+          options = {
+            programs.crush = {
+              enable = lib.mkEnableOption "Enable crush";
+              settings = crushOptions;
+            };
+          };
+
+          config = lib.mkIf config.programs.crush.enable {
+            environment.systemPackages = [ self.packages.${pkgs.system}.default ];
+
+            environment.etc."crush/crush.json" = lib.mkIf (config.programs.crush.settings != { }) {
+              text = builtins.toJSON config.programs.crush.settings;
+              mode = "0644";
+            };
+          };
+        };
+
+      homeManagerModules.default =
+        {
+          config,
+          lib,
+          pkgs,
+          ...
+        }:
+        let
+          crushOptions = import ./nix/options.nix { inherit lib; };
+        in
+        {
+          options = {
+            programs.crush = {
+              enable = lib.mkEnableOption "Enable crush";
+              settings = crushOptions;
+            };
+          };
+
+          config = lib.mkIf config.programs.crush.enable {
+            home.packages = [ self.packages.${pkgs.system}.default ];
+
+            home.file.".config/crush/crush.json" = lib.mkIf (config.programs.crush.settings != { }) {
+              text = builtins.toJSON config.programs.crush.settings;
+            };
+          };
+        };
+
+      packages = forAllSystems (
+        { pkgs }:
+        let
+          version = if self ? rev then self.rev else "dirty";
+          crush = pkgs.buildGoModule {
+            pname = "crush";
+            inherit version;
+            subPackages = [ "." ]; # Build from root directory
+            src = self;
+            vendorHash = null;
+
+            ldflags = [
+              "-s"
+              "-w"
+              "-X github.com/charmbracelet/crush/internal/version.Version=${version}"
+            ];
+
+            nativeBuildInputs = [ pkgs.installShellFiles ];
+
+            postInstall = ''
+              installShellCompletion --cmd crush \
+                --bash <($out/bin/crush completion bash) \
+                --fish <($out/bin/crush completion fish) \
+                --zsh <($out/bin/crush completion zsh)
+              
+              # Generate and install man page
+              $out/bin/crush man > crush.1
+              installManPage crush.1
+            '';
+
+            meta = with pkgs.lib; {
+              description = "A tool for building software with AI";
+              homepage = "https://github.com/charmbracelet/crush";
+              license = licenses.mit;
+              maintainers = with maintainers; [ taciturnaxolotl ];
+              platforms = platforms.linux ++ platforms.darwin;
+            };
+          };
+        in
+        {
+          default = crush;
+        }
+      );
+
+      apps = forAllSystems (
+        { pkgs }:
+        {
+          default = {
+            type = "app";
+            program = "${self.packages.${pkgs.system}.default}/bin/crush";
+          };
+        }
+      );
+
+      formatter = forAllSystems ({ pkgs }: pkgs.nixfmt-tree);
+    };
+}

nix/options.nix 🔗

@@ -0,0 +1,260 @@
+{ lib }:
+
+lib.mkOption {
+  type = lib.types.submodule {
+    options = {
+      providers = lib.mkOption {
+        type = lib.types.attrsOf (
+          lib.types.submodule {
+            options = {
+              name = lib.mkOption {
+                type = lib.types.str;
+                description = "Provider display name";
+              };
+              type = lib.mkOption {
+                type = lib.types.str;
+                default = "openai";
+                description = "Provider type (openai, anthropic, etc.)";
+              };
+              base_url = lib.mkOption {
+                type = lib.types.str;
+                default = "";
+                description = "Provider API endpoint";
+              };
+              api_key = lib.mkOption {
+                type = lib.types.str;
+                default = "";
+                description = "Provider API key";
+              };
+              system_prompt_prefix = lib.mkOption {
+                type = lib.types.str;
+                default = "";
+                description = "prefix for the system prompt";
+              };
+              extra_headers = lib.mkOption {
+                type = lib.types.attrsOf lib.types.str;
+                default = { };
+                description = "Extra HTTP headers for the provider";
+              };
+              models = lib.mkOption {
+                type = lib.types.listOf (
+                  lib.types.submodule {
+                    options = {
+                      id = lib.mkOption {
+                        type = lib.types.str;
+                        description = "Model ID";
+                      };
+                      model = lib.mkOption {
+                        type = lib.types.str;
+                        description = "Model display name";
+                      };
+                      cost_per_1m_in = lib.mkOption {
+                        type = lib.types.int;
+                        default = 0;
+                        description = "Cost per 1M input tokens";
+                      };
+                      cost_per_1m_out = lib.mkOption {
+                        type = lib.types.int;
+                        default = 0;
+                        description = "Cost per 1M output tokens";
+                      };
+                      cost_per_1m_in_cached = lib.mkOption {
+                        type = lib.types.int;
+                        default = 0;
+                        description = "Cost per 1M cached input tokens";
+                      };
+                      cost_per_1m_out_cached = lib.mkOption {
+                        type = lib.types.int;
+                        default = 0;
+                        description = "Cost per 1M cached output tokens";
+                      };
+                      context_window = lib.mkOption {
+                        type = lib.types.int;
+                        default = 128000;
+                        description = "Model context window size";
+                      };
+                      default_max_tokens = lib.mkOption {
+                        type = lib.types.int;
+                        default = 8192;
+                        description = "Default max tokens for responses";
+                      };
+                      can_reason = lib.mkOption {
+                        type = lib.types.bool;
+                        default = false;
+                        description = "Whether the model can reason";
+                      };
+                      has_reasoning_efforts = lib.mkOption {
+                        type = lib.types.bool;
+                        default = false;
+                        description = "Whether the model has reasoning efforts";
+                      };
+                      supports_attachments = lib.mkOption {
+                        type = lib.types.bool;
+                        default = false;
+                        description = "Whether the model supports attachments";
+                      };
+                    };
+                  }
+                );
+                default = [ ];
+                description = "Provider models configuration";
+              };
+              disable = lib.mkOption {
+                type = lib.types.bool;
+                default = false;
+                description = "Disable this provider";
+              };
+            };
+          }
+        );
+        default = { };
+        description = "Provider configurations";
+      };
+      lsp = lib.mkOption {
+        type = lib.types.attrsOf (
+          lib.types.submodule {
+            options = {
+              command = lib.mkOption {
+                type = lib.types.str;
+                description = "LSP command to execute";
+              };
+              args = lib.mkOption {
+                type = lib.types.listOf lib.types.str;
+                default = [ ];
+                description = "Arguments to pass to the LSP command";
+              };
+              disabled = lib.mkOption {
+                type = lib.types.bool;
+                default = false;
+                description = "Disable this LSP";
+              };
+            };
+          }
+        );
+        default = { };
+        description = "LSP configurations";
+      };
+      mcp = lib.mkOption {
+        type = lib.types.attrsOf (
+          lib.types.submodule {
+            options = {
+              type = lib.mkOption {
+                type = lib.types.str;
+                default = "http";
+                description = "MCP type (http, stdio, sse)";
+              };
+              url = lib.mkOption {
+                type = lib.types.str;
+                default = "";
+                description = "MCP URL for HTTP type";
+              };
+              command = lib.mkOption {
+                type = lib.types.str;
+                default = "";
+                description = "MCP command for stdio/sse type";
+              };
+              args = lib.mkOption {
+                type = lib.types.listOf lib.types.str;
+                default = [ ];
+                description = "Arguments for MCP command";
+              };
+              headers = lib.mkOption {
+                type = lib.types.attrsOf lib.types.str;
+                default = { };
+                description = "HTTP headers for MCP";
+              };
+              disabled = lib.mkOption {
+                type = lib.types.bool;
+                default = false;
+                description = "Disable this MCP";
+              };
+            };
+          }
+        );
+        default = { };
+        description = "MCP configurations";
+      };
+      options = lib.mkOption {
+        type = lib.types.submodule {
+          options = {
+            context_paths = lib.mkOption {
+              type = lib.types.listOf lib.types.str;
+              default = [ ];
+              description = "Additional context paths";
+            };
+            tui = lib.mkOption {
+              type = lib.types.submodule {
+                options = {
+                  compact_mode = lib.mkOption {
+                    type = lib.types.bool;
+                    default = false;
+                    description = "Enable compact mode in the TUI";
+                  };
+                };
+              };
+              default = { };
+              description = "TUI options";
+            };
+            debug = lib.mkOption {
+              type = lib.types.bool;
+              default = false;
+              description = "Enable debug mode";
+            };
+            debug_lsp = lib.mkOption {
+              type = lib.types.bool;
+              default = false;
+              description = "Enable LSP debugging";
+            };
+            disable_auto_summarize = lib.mkOption {
+              type = lib.types.bool;
+              default = false;
+              description = "Disable automatic summarization";
+            };
+            data_directory = lib.mkOption {
+              type = lib.types.str;
+              default = ".crush";
+              description = "Data directory relative to working directory";
+            };
+          };
+        };
+        default = { };
+        description = "General options";
+      };
+      models = lib.mkOption {
+        type = lib.types.attrsOf (
+          lib.types.submodule {
+            options = {
+              model = lib.mkOption {
+                type = lib.types.str;
+                description = "Model ID as used by the provider API";
+              };
+              provider = lib.mkOption {
+                type = lib.types.str;
+                description = "Model provider ID";
+              };
+              reasoning_effort = lib.mkOption {
+                type = lib.types.str;
+                default = "";
+                description = "Reasoning effort for OpenAI models";
+              };
+              max_tokens = lib.mkOption {
+                type = lib.types.int;
+                default = 0;
+                description = "Override default max tokens";
+              };
+              think = lib.mkOption {
+                type = lib.types.bool;
+                default = false;
+                description = "Enable thinking for Anthropic models";
+              };
+            };
+          }
+        );
+        default = { };
+        description = "Model configurations";
+      };
+    };
+  };
+  default = { };
+  description = "Crush configuration options";
+}