claude-code: split module files
Move public option declarations and file-entry helpers into dedicated files while keeping module assembly in default.nix. Update the AI labeler for the new module directory.
This commit is contained in:
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
- modules/programs/aiac.nix
|
- modules/programs/aiac.nix
|
||||||
- modules/programs/aichat.nix
|
- modules/programs/aichat.nix
|
||||||
- modules/programs/aider-chat.nix
|
- modules/programs/aider-chat.nix
|
||||||
- modules/programs/claude-code.nix
|
- modules/programs/claude-code/**/*
|
||||||
- modules/programs/codex.nix
|
- modules/programs/codex.nix
|
||||||
- modules/programs/fabric-ai.nix
|
- modules/programs/fabric-ai.nix
|
||||||
- modules/programs/antigravity-cli.nix
|
- modules/programs/antigravity-cli.nix
|
||||||
|
|||||||
@@ -0,0 +1,291 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
inherit (lib)
|
||||||
|
mkChangedOptionModule
|
||||||
|
nameValuePair
|
||||||
|
optionalAttrs
|
||||||
|
;
|
||||||
|
|
||||||
|
cfg = config.programs.claude-code;
|
||||||
|
|
||||||
|
jsonFormat = pkgs.formats.json { };
|
||||||
|
claudeCodeLib = import ./lib.nix { inherit lib pkgs; };
|
||||||
|
|
||||||
|
packageVersion = if cfg.package == null then null else lib.getVersion cfg.package;
|
||||||
|
hasPackageVersion = packageVersion != null && packageVersion != "";
|
||||||
|
|
||||||
|
# A null package has no detectable version, so assume the latest Claude Code
|
||||||
|
# and enable version-gated behavior by default. Preserve the legacy wrapper
|
||||||
|
# for non-null packages without version metadata.
|
||||||
|
atLeast =
|
||||||
|
version: cfg.package == null || (hasPackageVersion && lib.versionAtLeast packageVersion version);
|
||||||
|
supportsPluginDir = !hasPackageVersion || atLeast "2.1.76";
|
||||||
|
supportsPersonalPlugins = atLeast "2.1.157";
|
||||||
|
|
||||||
|
upstreamConfigDir = "${config.home.homeDirectory}/.claude";
|
||||||
|
|
||||||
|
isMcpServerEnabled =
|
||||||
|
server:
|
||||||
|
let
|
||||||
|
enabled = server.enabled or null;
|
||||||
|
disabled = (server.disabled or false) == true;
|
||||||
|
in
|
||||||
|
enabled != false && !disabled;
|
||||||
|
|
||||||
|
transformMcpServer =
|
||||||
|
name: server:
|
||||||
|
lib.hm.mcp.transformMcpServer {
|
||||||
|
inherit server;
|
||||||
|
exclude = [ "enabled" ];
|
||||||
|
extraTransforms = [
|
||||||
|
lib.hm.mcp.addType
|
||||||
|
(lib.hm.mcp.wrapEnvFilesCommand { inherit pkgs name; }) # envFiles currently still need wrapping https://github.com/anthropics/claude-code/issues/28942
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Shared MCP servers participate only when both integrations are enabled.
|
||||||
|
sharedMcpServers = lib.optionalAttrs (
|
||||||
|
cfg.enableMcpIntegration && config.programs.mcp.enable
|
||||||
|
) config.programs.mcp.servers;
|
||||||
|
|
||||||
|
transformedMcpServers = lib.mapAttrs transformMcpServer sharedMcpServers;
|
||||||
|
|
||||||
|
disabledMcpServerNames = lib.attrNames (
|
||||||
|
lib.filterAttrs (_: server: !(isMcpServerEnabled server)) (sharedMcpServers // cfg.mcpServers)
|
||||||
|
);
|
||||||
|
|
||||||
|
in
|
||||||
|
{
|
||||||
|
meta.maintainers = [ lib.maintainers.khaneliman ];
|
||||||
|
|
||||||
|
imports = [
|
||||||
|
(mkChangedOptionModule
|
||||||
|
[ "programs" "claude-code" "memory" "text" ]
|
||||||
|
[ "programs" "claude-code" "context" ]
|
||||||
|
(config: lib.getAttrFromPath [ "programs" "claude-code" "memory" "text" ] config)
|
||||||
|
)
|
||||||
|
(mkChangedOptionModule
|
||||||
|
[ "programs" "claude-code" "memory" "source" ]
|
||||||
|
[ "programs" "claude-code" "context" ]
|
||||||
|
(config: lib.getAttrFromPath [ "programs" "claude-code" "memory" "source" ] config)
|
||||||
|
)
|
||||||
|
(mkChangedOptionModule
|
||||||
|
[ "programs" "claude-code" "skillsDir" ]
|
||||||
|
[ "programs" "claude-code" "skills" ]
|
||||||
|
(config: lib.getAttrFromPath [ "programs" "claude-code" "skillsDir" ] config)
|
||||||
|
)
|
||||||
|
./options.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
config =
|
||||||
|
let
|
||||||
|
helpers = claudeCodeLib.mkHelpers { inherit (cfg) configDir; };
|
||||||
|
inherit (helpers)
|
||||||
|
mkHookEntries
|
||||||
|
mkInstalledMarketplaceEntry
|
||||||
|
mkMarkdownEntries
|
||||||
|
mkMarketplaceEntry
|
||||||
|
mkPluginEntry
|
||||||
|
mkRecursiveDirAttrs
|
||||||
|
mkSkillEntry
|
||||||
|
;
|
||||||
|
|
||||||
|
mergedMcpServers =
|
||||||
|
transformedMcpServers
|
||||||
|
// lib.mapAttrs (_: server: removeAttrs (lib.hm.mcp.addType server) [ "enabled" ]) cfg.mcpServers;
|
||||||
|
|
||||||
|
generatedPluginFiles =
|
||||||
|
lib.optional (mergedMcpServers != { }) {
|
||||||
|
name = ".mcp.json";
|
||||||
|
path = jsonFormat.generate "claude-code-mcp.json" { mcpServers = mergedMcpServers; };
|
||||||
|
}
|
||||||
|
++ lib.optional (cfg.lspServers != { }) {
|
||||||
|
name = ".lsp.json";
|
||||||
|
path = jsonFormat.generate "claude-code-lsp.json" cfg.lspServers;
|
||||||
|
};
|
||||||
|
|
||||||
|
generatedPlugin = 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.concatLines (
|
||||||
|
map (pluginFile: "install -Dm644 ${pluginFile.path} $out/${pluginFile.name}") generatedPluginFiles
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
pluginEntries =
|
||||||
|
lib.optional (generatedPluginFiles != [ ]) {
|
||||||
|
name = "claude-code-home-manager";
|
||||||
|
source = generatedPlugin;
|
||||||
|
}
|
||||||
|
++ map mkPluginEntry cfg.plugins;
|
||||||
|
|
||||||
|
legacyPluginPaths = lib.optional (generatedPluginFiles != [ ]) generatedPlugin ++ cfg.plugins;
|
||||||
|
|
||||||
|
hasManagedPlugins = legacyPluginPaths != [ ];
|
||||||
|
useLegacyPluginWrapper = hasManagedPlugins && !supportsPersonalPlugins;
|
||||||
|
|
||||||
|
legacyWrapperArgs = lib.flatten (
|
||||||
|
map (plugin: [
|
||||||
|
"--plugin-dir"
|
||||||
|
"${plugin}"
|
||||||
|
]) legacyPluginPaths
|
||||||
|
);
|
||||||
|
|
||||||
|
legacyFinalPackage = pkgs.symlinkJoin {
|
||||||
|
name = "claude-code";
|
||||||
|
paths = [ cfg.package ];
|
||||||
|
postBuild = ''
|
||||||
|
mv $out/bin/claude $out/bin/.claude-wrapped
|
||||||
|
cat > $out/bin/claude <<EOF
|
||||||
|
#! ${pkgs.bash}/bin/bash -e
|
||||||
|
exec -a "\$0" "$out/bin/.claude-wrapped" ${lib.escapeShellArgs legacyWrapperArgs} "\$@"
|
||||||
|
EOF
|
||||||
|
chmod +x $out/bin/claude
|
||||||
|
'';
|
||||||
|
inherit (cfg.package) meta;
|
||||||
|
};
|
||||||
|
|
||||||
|
pluginNames = map (plugin: plugin.name) pluginEntries;
|
||||||
|
|
||||||
|
skillNames =
|
||||||
|
if builtins.isAttrs cfg.skills then
|
||||||
|
lib.attrNames cfg.skills
|
||||||
|
else if lib.hm.strings.isPathLike cfg.skills && lib.pathIsDirectory cfg.skills then
|
||||||
|
lib.attrNames (builtins.readDir cfg.skills)
|
||||||
|
else
|
||||||
|
[ ];
|
||||||
|
|
||||||
|
pluginFileEntries = lib.optionalAttrs supportsPersonalPlugins (
|
||||||
|
lib.listToAttrs (
|
||||||
|
map (
|
||||||
|
plugin:
|
||||||
|
nameValuePair "${cfg.configDir}/skills/${plugin.name}" {
|
||||||
|
inherit (plugin) source;
|
||||||
|
recursive = true;
|
||||||
|
}
|
||||||
|
) pluginEntries
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
in
|
||||||
|
lib.mkIf cfg.enable {
|
||||||
|
warnings = lib.optional (useLegacyPluginWrapper && supportsPluginDir) ''
|
||||||
|
`programs.claude-code.package` ${
|
||||||
|
if hasPackageVersion then "version ${packageVersion}" else "has no detectable version and"
|
||||||
|
}
|
||||||
|
uses the legacy `--plugin-dir` wrapper. Strict-parser subcommands such
|
||||||
|
as `claude rc` may reject managed MCP, LSP, or plugin arguments. Upgrade
|
||||||
|
Claude Code to version 2.1.157 or later to use persistent personal
|
||||||
|
plugins instead.
|
||||||
|
'';
|
||||||
|
|
||||||
|
assertions =
|
||||||
|
let
|
||||||
|
exclusiveInlineDirNames = [
|
||||||
|
"rules"
|
||||||
|
"agents"
|
||||||
|
"commands"
|
||||||
|
"hooks"
|
||||||
|
];
|
||||||
|
|
||||||
|
mkExclusiveAssertion = inline: {
|
||||||
|
assertion = !(cfg.${inline} != { } && cfg.${inline + "Dir"} != null);
|
||||||
|
message = "Cannot specify both `programs.claude-code.${inline}` and `programs.claude-code.${inline}Dir`";
|
||||||
|
};
|
||||||
|
in
|
||||||
|
[
|
||||||
|
{
|
||||||
|
assertion = !hasManagedPlugins || supportsPluginDir;
|
||||||
|
message = "Managed Claude Code MCP, LSP, and plugins require `programs.claude-code.package` version 2.1.76 or later";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = !lib.hm.strings.isPathLike cfg.skills || lib.pathIsDirectory cfg.skills;
|
||||||
|
message = "`programs.claude-code.skills` must be a directory when set to a path";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion =
|
||||||
|
!supportsPersonalPlugins || lib.length pluginNames == lib.length (lib.unique pluginNames);
|
||||||
|
message = "`programs.claude-code.plugins` entries must resolve to unique personal-plugin directory names";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = !supportsPersonalPlugins || lib.intersectLists skillNames pluginNames == [ ];
|
||||||
|
message = "`programs.claude-code.skills` and managed plugins must have unique directory names";
|
||||||
|
}
|
||||||
|
]
|
||||||
|
++ map mkExclusiveAssertion exclusiveInlineDirNames;
|
||||||
|
|
||||||
|
programs.claude-code.finalPackage = lib.mkIf (cfg.package != null) (
|
||||||
|
if useLegacyPluginWrapper then legacyFinalPackage else cfg.package
|
||||||
|
);
|
||||||
|
|
||||||
|
home = {
|
||||||
|
packages = lib.mkIf (cfg.package != null) [ cfg.finalPackage ];
|
||||||
|
|
||||||
|
sessionVariables = lib.mkIf (cfg.configDir != upstreamConfigDir) {
|
||||||
|
CLAUDE_CONFIG_DIR = cfg.configDir;
|
||||||
|
};
|
||||||
|
|
||||||
|
file = lib.mkMerge [
|
||||||
|
(lib.mkIf (cfg.settings != { } || cfg.marketplaces != { } || disabledMcpServerNames != [ ]) {
|
||||||
|
"${cfg.configDir}/settings.json".source = jsonFormat.generate "claude-code-settings.json" (
|
||||||
|
cfg.settings
|
||||||
|
// {
|
||||||
|
"$schema" = "https://json.schemastore.org/claude-code-settings.json";
|
||||||
|
}
|
||||||
|
// optionalAttrs (cfg.marketplaces != { }) {
|
||||||
|
extraKnownMarketplaces = lib.mapAttrs mkMarketplaceEntry cfg.marketplaces;
|
||||||
|
}
|
||||||
|
// optionalAttrs (disabledMcpServerNames != [ ]) {
|
||||||
|
disabledMcpjsonServers = lib.unique (
|
||||||
|
(cfg.settings.disabledMcpjsonServers or [ ]) ++ disabledMcpServerNames
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
(
|
||||||
|
if lib.isPath cfg.context then
|
||||||
|
{
|
||||||
|
"${cfg.configDir}/CLAUDE.md".source = cfg.context;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
(lib.mkIf (cfg.context != "") {
|
||||||
|
"${cfg.configDir}/CLAUDE.md".text = cfg.context;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
(lib.mkIf (cfg.marketplaces != { }) {
|
||||||
|
"${cfg.configDir}/plugins/known_marketplaces.json".source =
|
||||||
|
jsonFormat.generate "claude-code-known-marketplaces.json" (
|
||||||
|
lib.mapAttrs mkInstalledMarketplaceEntry cfg.marketplaces
|
||||||
|
);
|
||||||
|
})
|
||||||
|
(mkMarkdownEntries "agents" cfg.agents)
|
||||||
|
(mkMarkdownEntries "commands" cfg.commands)
|
||||||
|
(mkMarkdownEntries "rules" cfg.rules)
|
||||||
|
(mkRecursiveDirAttrs "agents" cfg.agentsDir)
|
||||||
|
(mkRecursiveDirAttrs "commands" cfg.commandsDir)
|
||||||
|
(mkRecursiveDirAttrs "hooks" cfg.hooksDir)
|
||||||
|
(mkRecursiveDirAttrs "rules" cfg.rulesDir)
|
||||||
|
(lib.mkIf (lib.hm.strings.isPathLike cfg.skills) {
|
||||||
|
"${cfg.configDir}/skills" = {
|
||||||
|
source = cfg.skills;
|
||||||
|
recursive = true;
|
||||||
|
};
|
||||||
|
})
|
||||||
|
pluginFileEntries
|
||||||
|
(mkHookEntries cfg.hooks)
|
||||||
|
(lib.optionalAttrs (builtins.isAttrs cfg.skills) (lib.mapAttrs' mkSkillEntry cfg.skills))
|
||||||
|
(mkMarkdownEntries "output-styles" cfg.outputStyles)
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
{ lib, pkgs }:
|
||||||
|
let
|
||||||
|
jsonFormat = pkgs.formats.json { };
|
||||||
|
in
|
||||||
|
{
|
||||||
|
mkHelpers =
|
||||||
|
{ configDir }:
|
||||||
|
let
|
||||||
|
mkSourceEntry = content: if lib.isPath content then { source = content; } else { text = content; };
|
||||||
|
|
||||||
|
mkMarketplaceEntry = _name: content: {
|
||||||
|
source = {
|
||||||
|
source = "directory";
|
||||||
|
path = content;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit mkMarketplaceEntry;
|
||||||
|
|
||||||
|
mkHookEntries =
|
||||||
|
attrs:
|
||||||
|
lib.mapAttrs' (
|
||||||
|
name: content:
|
||||||
|
lib.nameValuePair "${configDir}/hooks/${name}" {
|
||||||
|
text = content;
|
||||||
|
executable = true;
|
||||||
|
}
|
||||||
|
) attrs;
|
||||||
|
|
||||||
|
mkInstalledMarketplaceEntry =
|
||||||
|
name: content:
|
||||||
|
(mkMarketplaceEntry name content)
|
||||||
|
// {
|
||||||
|
installLocation = content;
|
||||||
|
lastUpdated = "1970-01-01T00:00:00Z";
|
||||||
|
};
|
||||||
|
|
||||||
|
mkMarkdownEntries =
|
||||||
|
subdir: attrs:
|
||||||
|
lib.mapAttrs' (
|
||||||
|
name: content: lib.nameValuePair "${configDir}/${subdir}/${name}.md" (mkSourceEntry content)
|
||||||
|
) attrs;
|
||||||
|
|
||||||
|
mkPluginEntry =
|
||||||
|
plugin:
|
||||||
|
let
|
||||||
|
name = builtins.unsafeDiscardStringContext (
|
||||||
|
lib.strings.sanitizeDerivationName (baseNameOf (toString plugin))
|
||||||
|
);
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit name;
|
||||||
|
source = pkgs.symlinkJoin {
|
||||||
|
name = "claude-code-${name}";
|
||||||
|
paths = [ plugin ];
|
||||||
|
postBuild = ''
|
||||||
|
if [[ ! -e $out/.claude-plugin/plugin.json ]]; then
|
||||||
|
install -Dm644 ${
|
||||||
|
jsonFormat.generate "claude-code-${name}.json" {
|
||||||
|
inherit name;
|
||||||
|
}
|
||||||
|
} $out/.claude-plugin/plugin.json
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
mkRecursiveDirAttrs =
|
||||||
|
subdir: dir:
|
||||||
|
lib.optionalAttrs (dir != null) {
|
||||||
|
"${configDir}/${subdir}" = {
|
||||||
|
source = dir;
|
||||||
|
recursive = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
mkSkillEntry =
|
||||||
|
name: content:
|
||||||
|
if lib.hm.strings.isPathLike content && lib.pathIsDirectory content then
|
||||||
|
lib.nameValuePair "${configDir}/skills/${name}" {
|
||||||
|
source = content;
|
||||||
|
recursive = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
lib.nameValuePair "${configDir}/skills/${name}/SKILL.md" (
|
||||||
|
if lib.hm.strings.isPathLike content then { source = content; } else { text = content; }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -5,61 +5,10 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
inherit (lib)
|
inherit (lib) literalExpression mkOption optionalAttrs;
|
||||||
literalExpression
|
|
||||||
mkChangedOptionModule
|
|
||||||
mkOption
|
|
||||||
nameValuePair
|
|
||||||
optionalAttrs
|
|
||||||
;
|
|
||||||
|
|
||||||
cfg = config.programs.claude-code;
|
|
||||||
|
|
||||||
jsonFormat = pkgs.formats.json { };
|
jsonFormat = pkgs.formats.json { };
|
||||||
|
|
||||||
packageVersion = if cfg.package == null then null else lib.getVersion cfg.package;
|
|
||||||
hasPackageVersion = packageVersion != null && packageVersion != "";
|
|
||||||
|
|
||||||
# A null package has no detectable version, so assume the latest Claude Code
|
|
||||||
# and enable version-gated behavior by default. Preserve the legacy wrapper
|
|
||||||
# for non-null packages without version metadata.
|
|
||||||
atLeast =
|
|
||||||
version: cfg.package == null || (hasPackageVersion && lib.versionAtLeast packageVersion version);
|
|
||||||
supportsPluginDir = !hasPackageVersion || atLeast "2.1.76";
|
|
||||||
supportsPersonalPlugins = atLeast "2.1.157";
|
|
||||||
|
|
||||||
upstreamConfigDir = "${config.home.homeDirectory}/.claude";
|
|
||||||
|
|
||||||
isMcpServerEnabled =
|
|
||||||
server:
|
|
||||||
let
|
|
||||||
enabled = server.enabled or null;
|
|
||||||
disabled = (server.disabled or false) == true;
|
|
||||||
in
|
|
||||||
enabled != false && !disabled;
|
|
||||||
|
|
||||||
transformMcpServer =
|
|
||||||
name: server:
|
|
||||||
lib.hm.mcp.transformMcpServer {
|
|
||||||
inherit server;
|
|
||||||
exclude = [ "enabled" ];
|
|
||||||
extraTransforms = [
|
|
||||||
lib.hm.mcp.addType
|
|
||||||
(lib.hm.mcp.wrapEnvFilesCommand { inherit pkgs name; }) # envFiles currently still need wrapping https://github.com/anthropics/claude-code/issues/28942
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
# Shared MCP servers participate only when both integrations are enabled.
|
|
||||||
sharedMcpServers = lib.optionalAttrs (
|
|
||||||
cfg.enableMcpIntegration && config.programs.mcp.enable
|
|
||||||
) config.programs.mcp.servers;
|
|
||||||
|
|
||||||
transformedMcpServers = lib.mapAttrs transformMcpServer sharedMcpServers;
|
|
||||||
|
|
||||||
disabledMcpServerNames = lib.attrNames (
|
|
||||||
lib.filterAttrs (_: server: !(isMcpServerEnabled server)) (sharedMcpServers // cfg.mcpServers)
|
|
||||||
);
|
|
||||||
|
|
||||||
mkContentOption =
|
mkContentOption =
|
||||||
{
|
{
|
||||||
description,
|
description,
|
||||||
@@ -81,29 +30,8 @@ let
|
|||||||
default = null;
|
default = null;
|
||||||
inherit description example;
|
inherit description example;
|
||||||
};
|
};
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
meta.maintainers = [ lib.maintainers.khaneliman ];
|
|
||||||
|
|
||||||
imports = [
|
|
||||||
(mkChangedOptionModule
|
|
||||||
[ "programs" "claude-code" "memory" "text" ]
|
|
||||||
[ "programs" "claude-code" "context" ]
|
|
||||||
(config: lib.getAttrFromPath [ "programs" "claude-code" "memory" "text" ] config)
|
|
||||||
)
|
|
||||||
(mkChangedOptionModule
|
|
||||||
[ "programs" "claude-code" "memory" "source" ]
|
|
||||||
[ "programs" "claude-code" "context" ]
|
|
||||||
(config: lib.getAttrFromPath [ "programs" "claude-code" "memory" "source" ] config)
|
|
||||||
)
|
|
||||||
(mkChangedOptionModule
|
|
||||||
[ "programs" "claude-code" "skillsDir" ]
|
|
||||||
[ "programs" "claude-code" "skills" ]
|
|
||||||
(config: lib.getAttrFromPath [ "programs" "claude-code" "skillsDir" ] config)
|
|
||||||
)
|
|
||||||
];
|
|
||||||
|
|
||||||
options.programs.claude-code = {
|
options.programs.claude-code = {
|
||||||
enable = lib.mkEnableOption "Claude Code, Anthropic's official CLI";
|
enable = lib.mkEnableOption "Claude Code, Anthropic's official CLI";
|
||||||
|
|
||||||
@@ -138,7 +66,7 @@ in
|
|||||||
|
|
||||||
configDir = mkOption {
|
configDir = mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = upstreamConfigDir;
|
default = "${config.home.homeDirectory}/.claude";
|
||||||
defaultText = literalExpression ''"''${config.home.homeDirectory}/.claude"'';
|
defaultText = literalExpression ''"''${config.home.homeDirectory}/.claude"'';
|
||||||
example = literalExpression ''"''${config.xdg.configHome}/claude"'';
|
example = literalExpression ''"''${config.xdg.configHome}/claude"'';
|
||||||
description = ''
|
description = ''
|
||||||
@@ -582,278 +510,4 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config =
|
|
||||||
let
|
|
||||||
mkSourceEntry = content: if lib.isPath content then { source = content; } else { text = content; };
|
|
||||||
|
|
||||||
mkMarkdownEntries =
|
|
||||||
subdir: attrs:
|
|
||||||
lib.mapAttrs' (
|
|
||||||
name: content: nameValuePair "${cfg.configDir}/${subdir}/${name}.md" (mkSourceEntry content)
|
|
||||||
) attrs;
|
|
||||||
|
|
||||||
mkHookEntries =
|
|
||||||
attrs:
|
|
||||||
lib.mapAttrs' (
|
|
||||||
name: content:
|
|
||||||
nameValuePair "${cfg.configDir}/hooks/${name}" {
|
|
||||||
text = content;
|
|
||||||
executable = true;
|
|
||||||
}
|
|
||||||
) attrs;
|
|
||||||
|
|
||||||
mkRecursiveDirAttrs =
|
|
||||||
subdir: dir:
|
|
||||||
optionalAttrs (dir != null) {
|
|
||||||
"${cfg.configDir}/${subdir}" = {
|
|
||||||
source = dir;
|
|
||||||
recursive = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
mkSkillEntry =
|
|
||||||
name: content:
|
|
||||||
if lib.hm.strings.isPathLike content && lib.pathIsDirectory content then
|
|
||||||
nameValuePair "${cfg.configDir}/skills/${name}" {
|
|
||||||
source = content;
|
|
||||||
recursive = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
nameValuePair "${cfg.configDir}/skills/${name}/SKILL.md" (
|
|
||||||
if lib.hm.strings.isPathLike content then { source = content; } else { text = content; }
|
|
||||||
);
|
|
||||||
|
|
||||||
mkMarketplaceEntry = _name: content: {
|
|
||||||
source = {
|
|
||||||
source = "directory";
|
|
||||||
path = content;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
mkInstalledMarketplaceEntry =
|
|
||||||
name: content:
|
|
||||||
(mkMarketplaceEntry name content)
|
|
||||||
// {
|
|
||||||
installLocation = content;
|
|
||||||
lastUpdated = "1970-01-01T00:00:00Z";
|
|
||||||
};
|
|
||||||
|
|
||||||
mergedMcpServers =
|
|
||||||
transformedMcpServers
|
|
||||||
// lib.mapAttrs (_: server: removeAttrs (lib.hm.mcp.addType server) [ "enabled" ]) cfg.mcpServers;
|
|
||||||
|
|
||||||
generatedPluginFiles =
|
|
||||||
lib.optional (mergedMcpServers != { }) {
|
|
||||||
name = ".mcp.json";
|
|
||||||
path = jsonFormat.generate "claude-code-mcp.json" { mcpServers = mergedMcpServers; };
|
|
||||||
}
|
|
||||||
++ lib.optional (cfg.lspServers != { }) {
|
|
||||||
name = ".lsp.json";
|
|
||||||
path = jsonFormat.generate "claude-code-lsp.json" cfg.lspServers;
|
|
||||||
};
|
|
||||||
|
|
||||||
generatedPlugin = 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.concatLines (
|
|
||||||
map (pluginFile: "install -Dm644 ${pluginFile.path} $out/${pluginFile.name}") generatedPluginFiles
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
mkPluginEntry =
|
|
||||||
plugin:
|
|
||||||
let
|
|
||||||
name = builtins.unsafeDiscardStringContext (
|
|
||||||
lib.strings.sanitizeDerivationName (baseNameOf (toString plugin))
|
|
||||||
);
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit name;
|
|
||||||
source = pkgs.symlinkJoin {
|
|
||||||
name = "claude-code-${name}";
|
|
||||||
paths = [ plugin ];
|
|
||||||
postBuild = ''
|
|
||||||
if [[ ! -e $out/.claude-plugin/plugin.json ]]; then
|
|
||||||
install -Dm644 ${
|
|
||||||
jsonFormat.generate "claude-code-${name}.json" {
|
|
||||||
inherit name;
|
|
||||||
}
|
|
||||||
} $out/.claude-plugin/plugin.json
|
|
||||||
fi
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
pluginEntries =
|
|
||||||
lib.optional (generatedPluginFiles != [ ]) {
|
|
||||||
name = "claude-code-home-manager";
|
|
||||||
source = generatedPlugin;
|
|
||||||
}
|
|
||||||
++ map mkPluginEntry cfg.plugins;
|
|
||||||
|
|
||||||
legacyPluginPaths = lib.optional (generatedPluginFiles != [ ]) generatedPlugin ++ cfg.plugins;
|
|
||||||
|
|
||||||
hasManagedPlugins = legacyPluginPaths != [ ];
|
|
||||||
useLegacyPluginWrapper = hasManagedPlugins && !supportsPersonalPlugins;
|
|
||||||
|
|
||||||
legacyWrapperArgs = lib.flatten (
|
|
||||||
map (plugin: [
|
|
||||||
"--plugin-dir"
|
|
||||||
"${plugin}"
|
|
||||||
]) legacyPluginPaths
|
|
||||||
);
|
|
||||||
|
|
||||||
legacyFinalPackage = pkgs.symlinkJoin {
|
|
||||||
name = "claude-code";
|
|
||||||
paths = [ cfg.package ];
|
|
||||||
postBuild = ''
|
|
||||||
mv $out/bin/claude $out/bin/.claude-wrapped
|
|
||||||
cat > $out/bin/claude <<EOF
|
|
||||||
#! ${pkgs.bash}/bin/bash -e
|
|
||||||
exec -a "\$0" "$out/bin/.claude-wrapped" ${lib.escapeShellArgs legacyWrapperArgs} "\$@"
|
|
||||||
EOF
|
|
||||||
chmod +x $out/bin/claude
|
|
||||||
'';
|
|
||||||
inherit (cfg.package) meta;
|
|
||||||
};
|
|
||||||
|
|
||||||
pluginNames = map (plugin: plugin.name) pluginEntries;
|
|
||||||
|
|
||||||
skillNames =
|
|
||||||
if builtins.isAttrs cfg.skills then
|
|
||||||
lib.attrNames cfg.skills
|
|
||||||
else if lib.hm.strings.isPathLike cfg.skills && lib.pathIsDirectory cfg.skills then
|
|
||||||
lib.attrNames (builtins.readDir cfg.skills)
|
|
||||||
else
|
|
||||||
[ ];
|
|
||||||
|
|
||||||
pluginFileEntries = lib.optionalAttrs supportsPersonalPlugins (
|
|
||||||
lib.listToAttrs (
|
|
||||||
map (
|
|
||||||
plugin:
|
|
||||||
nameValuePair "${cfg.configDir}/skills/${plugin.name}" {
|
|
||||||
inherit (plugin) source;
|
|
||||||
recursive = true;
|
|
||||||
}
|
|
||||||
) pluginEntries
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
in
|
|
||||||
lib.mkIf cfg.enable {
|
|
||||||
warnings = lib.optional (useLegacyPluginWrapper && supportsPluginDir) ''
|
|
||||||
`programs.claude-code.package` ${
|
|
||||||
if hasPackageVersion then "version ${packageVersion}" else "has no detectable version and"
|
|
||||||
}
|
|
||||||
uses the legacy `--plugin-dir` wrapper. Strict-parser subcommands such
|
|
||||||
as `claude rc` may reject managed MCP, LSP, or plugin arguments. Upgrade
|
|
||||||
Claude Code to version 2.1.157 or later to use persistent personal
|
|
||||||
plugins instead.
|
|
||||||
'';
|
|
||||||
|
|
||||||
assertions =
|
|
||||||
let
|
|
||||||
exclusiveInlineDirNames = [
|
|
||||||
"rules"
|
|
||||||
"agents"
|
|
||||||
"commands"
|
|
||||||
"hooks"
|
|
||||||
];
|
|
||||||
|
|
||||||
mkExclusiveAssertion = inline: {
|
|
||||||
assertion = !(cfg.${inline} != { } && cfg.${inline + "Dir"} != null);
|
|
||||||
message = "Cannot specify both `programs.claude-code.${inline}` and `programs.claude-code.${inline}Dir`";
|
|
||||||
};
|
|
||||||
in
|
|
||||||
[
|
|
||||||
{
|
|
||||||
assertion = !hasManagedPlugins || supportsPluginDir;
|
|
||||||
message = "Managed Claude Code MCP, LSP, and plugins require `programs.claude-code.package` version 2.1.76 or later";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
assertion = !lib.hm.strings.isPathLike cfg.skills || lib.pathIsDirectory cfg.skills;
|
|
||||||
message = "`programs.claude-code.skills` must be a directory when set to a path";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
assertion =
|
|
||||||
!supportsPersonalPlugins || lib.length pluginNames == lib.length (lib.unique pluginNames);
|
|
||||||
message = "`programs.claude-code.plugins` entries must resolve to unique personal-plugin directory names";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
assertion = !supportsPersonalPlugins || lib.intersectLists skillNames pluginNames == [ ];
|
|
||||||
message = "`programs.claude-code.skills` and managed plugins must have unique directory names";
|
|
||||||
}
|
|
||||||
]
|
|
||||||
++ map mkExclusiveAssertion exclusiveInlineDirNames;
|
|
||||||
|
|
||||||
programs.claude-code.finalPackage = lib.mkIf (cfg.package != null) (
|
|
||||||
if useLegacyPluginWrapper then legacyFinalPackage else cfg.package
|
|
||||||
);
|
|
||||||
|
|
||||||
home = {
|
|
||||||
packages = lib.mkIf (cfg.package != null) [ cfg.finalPackage ];
|
|
||||||
|
|
||||||
sessionVariables = lib.mkIf (cfg.configDir != upstreamConfigDir) {
|
|
||||||
CLAUDE_CONFIG_DIR = cfg.configDir;
|
|
||||||
};
|
|
||||||
|
|
||||||
file = lib.mkMerge [
|
|
||||||
(lib.mkIf (cfg.settings != { } || cfg.marketplaces != { } || disabledMcpServerNames != [ ]) {
|
|
||||||
"${cfg.configDir}/settings.json".source = jsonFormat.generate "claude-code-settings.json" (
|
|
||||||
cfg.settings
|
|
||||||
// {
|
|
||||||
"$schema" = "https://json.schemastore.org/claude-code-settings.json";
|
|
||||||
}
|
|
||||||
// optionalAttrs (cfg.marketplaces != { }) {
|
|
||||||
extraKnownMarketplaces = lib.mapAttrs mkMarketplaceEntry cfg.marketplaces;
|
|
||||||
}
|
|
||||||
// optionalAttrs (disabledMcpServerNames != [ ]) {
|
|
||||||
disabledMcpjsonServers = lib.unique (
|
|
||||||
(cfg.settings.disabledMcpjsonServers or [ ]) ++ disabledMcpServerNames
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
})
|
|
||||||
(
|
|
||||||
if lib.isPath cfg.context then
|
|
||||||
{
|
|
||||||
"${cfg.configDir}/CLAUDE.md".source = cfg.context;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
(lib.mkIf (cfg.context != "") {
|
|
||||||
"${cfg.configDir}/CLAUDE.md".text = cfg.context;
|
|
||||||
})
|
|
||||||
)
|
|
||||||
(lib.mkIf (cfg.marketplaces != { }) {
|
|
||||||
"${cfg.configDir}/plugins/known_marketplaces.json".source =
|
|
||||||
jsonFormat.generate "claude-code-known-marketplaces.json" (
|
|
||||||
lib.mapAttrs mkInstalledMarketplaceEntry cfg.marketplaces
|
|
||||||
);
|
|
||||||
})
|
|
||||||
(mkMarkdownEntries "agents" cfg.agents)
|
|
||||||
(mkMarkdownEntries "commands" cfg.commands)
|
|
||||||
(mkMarkdownEntries "rules" cfg.rules)
|
|
||||||
(mkRecursiveDirAttrs "agents" cfg.agentsDir)
|
|
||||||
(mkRecursiveDirAttrs "commands" cfg.commandsDir)
|
|
||||||
(mkRecursiveDirAttrs "hooks" cfg.hooksDir)
|
|
||||||
(mkRecursiveDirAttrs "rules" cfg.rulesDir)
|
|
||||||
(lib.mkIf (lib.hm.strings.isPathLike cfg.skills) {
|
|
||||||
"${cfg.configDir}/skills" = {
|
|
||||||
source = cfg.skills;
|
|
||||||
recursive = true;
|
|
||||||
};
|
|
||||||
})
|
|
||||||
pluginFileEntries
|
|
||||||
(mkHookEntries cfg.hooks)
|
|
||||||
(lib.optionalAttrs (builtins.isAttrs cfg.skills) (lib.mapAttrs' mkSkillEntry cfg.skills))
|
|
||||||
(mkMarkdownEntries "output-styles" cfg.outputStyles)
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user