From 0d200e1da726a9ec73ee28d6a2d5ff44c37a9700 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Thu, 19 Mar 2026 00:00:09 -0500 Subject: [PATCH] claude-code: deduplicate module helpers Factor the repeated claude-code module patterns into a small set of local helpers instead of open-coding them over and over. This keeps the behavior the same, but removes duplication in the inline-or-path option declarations, the inline-vs-directory assertions, and the repeated .claude file generation code. The claude-code NMT test slice still passes after the refactor. Signed-off-by: Austin Horstman --- modules/programs/claude-code.nix | 367 +++++++++++++++---------------- 1 file changed, 181 insertions(+), 186 deletions(-) diff --git a/modules/programs/claude-code.nix b/modules/programs/claude-code.nix index 9700416e..dcb85add 100644 --- a/modules/programs/claude-code.nix +++ b/modules/programs/claude-code.nix @@ -7,9 +7,10 @@ let cfg = config.programs.claude-code; jsonFormat = pkgs.formats.json { }; + transformedMcpServers = lib.optionalAttrs (cfg.enableMcpIntegration && config.programs.mcp.enable) ( lib.mapAttrs ( - name: server: + _name: server: (removeAttrs server [ "disabled" ]) // (lib.optionalAttrs (server ? url) { type = "http"; }) // (lib.optionalAttrs (server ? command) { type = "stdio"; }) @@ -18,6 +19,29 @@ let } ) config.programs.mcp.servers ); + + mkContentOption = + { + description, + example ? null, + }: + lib.mkOption ( + { + type = lib.types.attrsOf (lib.types.either lib.types.lines lib.types.path); + default = { }; + inherit description; + } + // lib.optionalAttrs (example != null) { inherit example; } + ); + + mkDirOption = + { description, example }: + lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + inherit description example; + }; + in { meta.maintainers = [ lib.maintainers.khaneliman ]; @@ -104,9 +128,7 @@ in description = "JSON configuration for Claude Code settings.json"; }; - agents = lib.mkOption { - type = lib.types.attrsOf (lib.types.either lib.types.lines lib.types.path); - default = { }; + agents = mkContentOption { description = '' Custom agents for Claude Code. The attribute name becomes the agent filename, and the value is either: @@ -131,9 +153,7 @@ in ''; }; - commands = lib.mkOption { - type = lib.types.attrsOf (lib.types.either lib.types.lines lib.types.path); - default = { }; + commands = mkContentOption { description = '' Custom commands for Claude Code. The attribute name becomes the command filename, and the value is either: @@ -222,9 +242,7 @@ in }; }; - rules = lib.mkOption { - type = lib.types.attrsOf (lib.types.either lib.types.lines lib.types.path); - default = { }; + rules = mkContentOption { description = '' Modular rule files for Claude Code. The attribute name becomes the rule filename, and the value is either: @@ -252,9 +270,7 @@ in ''; }; - rulesDir = lib.mkOption { - type = lib.types.nullOr lib.types.path; - default = null; + rulesDir = mkDirOption { description = '' Path to a directory containing rule files for Claude Code. Rule files from this directory will be symlinked to .claude/rules/. @@ -263,9 +279,7 @@ in example = lib.literalExpression "./rules"; }; - agentsDir = lib.mkOption { - type = lib.types.nullOr lib.types.path; - default = null; + agentsDir = mkDirOption { description = '' Path to a directory containing agent files for Claude Code. Agent files from this directory will be symlinked to .claude/agents/. @@ -273,9 +287,7 @@ in example = lib.literalExpression "./agents"; }; - commandsDir = lib.mkOption { - type = lib.types.nullOr lib.types.path; - default = null; + commandsDir = mkDirOption { description = '' Path to a directory containing command files for Claude Code. Command files from this directory will be symlinked to .claude/commands/. @@ -283,9 +295,7 @@ in example = lib.literalExpression "./commands"; }; - hooksDir = lib.mkOption { - type = lib.types.nullOr lib.types.path; - default = null; + hooksDir = mkDirOption { description = '' Path to a directory containing hook files for Claude Code. Hook files from this directory will be symlinked to .claude/hooks/. @@ -293,9 +303,7 @@ in example = lib.literalExpression "./hooks"; }; - outputStyles = lib.mkOption { - type = lib.types.attrsOf (lib.types.either lib.types.lines lib.types.path); - default = { }; + outputStyles = mkContentOption { description = '' Custom output styles for Claude Code. The attribute name becomes the base of the output style filename. @@ -316,9 +324,7 @@ in ''; }; - skills = lib.mkOption { - type = lib.types.attrsOf (lib.types.either lib.types.lines lib.types.path); - default = { }; + skills = mkContentOption { description = '' Custom skills for Claude Code. The attribute name becomes the skill directory name, and the value is either: @@ -353,9 +359,7 @@ in ''; }; - skillsDir = lib.mkOption { - type = lib.types.nullOr lib.types.path; - default = null; + skillsDir = mkDirOption { description = '' Path to a directory containing skill directories for Claude Code. Each skill directory should contain a SKILL.md entrypoint file. @@ -432,166 +436,157 @@ in }; }; - config = lib.mkIf cfg.enable { - assertions = [ - { - assertion = - (cfg.mcpServers == { } && cfg.lspServers == { } && !cfg.enableMcpIntegration) - || cfg.package != null; - message = "`programs.claude-code.package` cannot be null when `mcpServers`, `lspServers`, or `enableMcpIntegration` is configured"; - } - { - assertion = !(cfg.memory.text != null && cfg.memory.source != null); - message = "Cannot specify both `programs.claude-code.memory.text` and `programs.claude-code.memory.source`"; - } - { - assertion = !(cfg.rules != { } && cfg.rulesDir != null); - message = "Cannot specify both `programs.claude-code.rules` and `programs.claude-code.rulesDir`"; - } - { - assertion = !(cfg.agents != { } && cfg.agentsDir != null); - message = "Cannot specify both `programs.claude-code.agents` and `programs.claude-code.agentsDir`"; - } - { - assertion = !(cfg.commands != { } && cfg.commandsDir != null); - message = "Cannot specify both `programs.claude-code.commands` and `programs.claude-code.commandsDir`"; - } - { - assertion = !(cfg.hooks != { } && cfg.hooksDir != null); - message = "Cannot specify both `programs.claude-code.hooks` and `programs.claude-code.hooksDir`"; - } - { - assertion = !(cfg.skills != { } && cfg.skillsDir != null); - message = "Cannot specify both `programs.claude-code.skills` and `programs.claude-code.skillsDir`"; - } - ]; + config = + let + mkSourceEntry = content: if lib.isPath content then { source = content; } else { text = content; }; - programs.claude-code.finalPackage = - let - mergedMcpServers = transformedMcpServers // cfg.mcpServers; - hasMcpServers = mergedMcpServers != { }; - hasLspServers = cfg.lspServers != { }; - pluginDir = - if hasMcpServers || hasLspServers then - pkgs.runCommand "claude-code-hm-plugin" { } '' - install -Dm644 ${ - jsonFormat.generate "claude-code-plugin.json" { - name = "claude-code-home-manager"; - } - } $out/.claude-plugin/plugin.json - ${lib.optionalString hasMcpServers '' - install -Dm644 ${ - jsonFormat.generate "claude-code-mcp.json" { mcpServers = mergedMcpServers; } - } $out/.mcp.json - ''} - ${lib.optionalString hasLspServers '' - install -Dm644 ${jsonFormat.generate "claude-code-lsp.json" cfg.lspServers} $out/.lsp.json - ''} - '' - else - null; - in - if pluginDir != null then - pkgs.symlinkJoin { - name = "claude-code"; - paths = [ cfg.package ]; - postBuild = '' - mv $out/bin/claude $out/bin/.claude-wrapped - cat > $out/bin/claude < $out/bin/claude <