From 7cb2c312816045fe42970dbad7f1ea33b9e46066 Mon Sep 17 00:00:00 2001 From: Kieran Klukas Date: Wed, 16 Jul 2025 17:41:17 -0400 Subject: [PATCH] feat: add nix flake has a home-manager module and nix module and supports configuring through nix as well as having auto shell completions, and a man page --- README.md | 79 +++++++++++++++ flake.lock | 27 +++++ flake.nix | 138 +++++++++++++++++++++++++ nix/options.nix | 260 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 504 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 nix/options.nix diff --git a/README.md b/README.md index 7f428d31ed38ba437d2df4edb6747452d9624e2b..766c49092e4df808c927540c16bf91e365802da4 100644 --- a/README.md +++ b/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 diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000000000000000000000000000000000000..05b1cf89221ec3d7b36c78540c23c015b5e8b19f --- /dev/null +++ b/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 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000000000000000000000000000000000000..4b88768c113432fc811aaa664b8f4efe4c0d37a1 --- /dev/null +++ b/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); + }; +} diff --git a/nix/options.nix b/nix/options.nix new file mode 100644 index 0000000000000000000000000000000000000000..1fe4e85917c9367072fca524164460082835589a --- /dev/null +++ b/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"; +}