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
@@ -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
@@ -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
+}
@@ -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);
+ };
+}
@@ -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";
+}