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/aichat.nix
|
||||
- modules/programs/aider-chat.nix
|
||||
- modules/programs/claude-code.nix
|
||||
- modules/programs/claude-code/**/*
|
||||
- modules/programs/codex.nix
|
||||
- modules/programs/fabric-ai.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
|
||||
inherit (lib)
|
||||
literalExpression
|
||||
mkChangedOptionModule
|
||||
mkOption
|
||||
nameValuePair
|
||||
optionalAttrs
|
||||
;
|
||||
|
||||
cfg = config.programs.claude-code;
|
||||
inherit (lib) literalExpression mkOption optionalAttrs;
|
||||
|
||||
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 =
|
||||
{
|
||||
description,
|
||||
@@ -81,29 +30,8 @@ let
|
||||
default = null;
|
||||
inherit description example;
|
||||
};
|
||||
|
||||
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 = {
|
||||
enable = lib.mkEnableOption "Claude Code, Anthropic's official CLI";
|
||||
|
||||
@@ -138,7 +66,7 @@ in
|
||||
|
||||
configDir = mkOption {
|
||||
type = lib.types.str;
|
||||
default = upstreamConfigDir;
|
||||
default = "${config.home.homeDirectory}/.claude";
|
||||
defaultText = literalExpression ''"''${config.home.homeDirectory}/.claude"'';
|
||||
example = literalExpression ''"''${config.xdg.configHome}/claude"'';
|
||||
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