mirror of
https://github.com/nix-community/home-manager.git
synced 2026-10-31 01:02:22 +00:00
Claude Code lists a plugin's `agents/` and `commands/` entries with a `readdir` that accepts only regular files, so symlinked directory entries are skipped. Managed plugins were linked recursively, which materializes a real directory holding one symlink per file, and wrapped with `pkgs.symlinkJoin`, which mirrors directories and symlinks the leaf files. Both layers dropped every agent and command a plugin provides. Link each plugin as a single directory symlink, and wrap it with a `runCommand` that links only top-level entries so component directories stay symlinks to the original. Also accept an attribute set for `plugins`, making the personal-plugin directory name user-controlled and stable rather than derived from the source's base name, which yields names such as `bxa1s0m3h4sh-source` that change whenever the plugin is updated. The list form keeps working, with a warning.
90 lines
3.0 KiB
Nix
90 lines
3.0 KiB
Nix
{ config, pkgs, ... }:
|
|
let
|
|
mkPackagePlugin =
|
|
name:
|
|
pkgs.runCommand "source" { } ''
|
|
install -Dm644 ${
|
|
pkgs.writeText "${name}.json" (
|
|
builtins.toJSON {
|
|
inherit name;
|
|
}
|
|
)
|
|
} $out/.claude-plugin/plugin.json
|
|
'';
|
|
manifestlessPlugin = pkgs.runCommand "manifestless-plugin" { } ''
|
|
install -Dm644 ${./test-command.md} $out/commands/test.md
|
|
'';
|
|
in
|
|
|
|
{
|
|
programs.claude-code = {
|
|
package = config.lib.test.mkStubPackage {
|
|
name = "claude-code";
|
|
version = "2.1.197";
|
|
buildScript = ''
|
|
mkdir -p $out/bin
|
|
cat > $out/bin/claude <<'EOF'
|
|
#!${pkgs.runtimeShell}
|
|
test "$#" -eq 1
|
|
test "$1" = rc
|
|
EOF
|
|
chmod +x $out/bin/claude
|
|
'';
|
|
};
|
|
enable = true;
|
|
|
|
plugins = {
|
|
test-plugin = ./test-plugin;
|
|
package-plugin-one = mkPackagePlugin "package-plugin-one";
|
|
package-plugin-two = mkPackagePlugin "package-plugin-two";
|
|
manifestless = manifestlessPlugin;
|
|
};
|
|
};
|
|
|
|
nmt.script = ''
|
|
"$TESTED/home-path/bin/claude" rc
|
|
assertPathNotExists "$TESTED/home-path/bin/.claude-wrapped"
|
|
|
|
# Each plugin must be linked as a single directory symlink. Linking
|
|
# recursively materializes a real directory holding one symlink per file,
|
|
# which makes Claude Code skip every agent and command the plugin ships.
|
|
assertLinkExists "home-files/.claude/skills/test-plugin"
|
|
assertLinkExists "home-files/.claude/skills/package-plugin-one"
|
|
assertLinkExists "home-files/.claude/skills/manifestless"
|
|
|
|
pluginDir="$TESTED/home-files/.claude/skills/test-plugin"
|
|
assertFileContent "$pluginDir/.claude-plugin/plugin.json" ${./test-plugin/.claude-plugin/plugin.json}
|
|
assertFileContent \
|
|
"$pluginDir/agents/test-plugin-agent.md" \
|
|
${./test-plugin/agents/test-plugin-agent.md}
|
|
assertFileContent \
|
|
"$pluginDir/commands/test-plugin-command.md" \
|
|
${./test-plugin/commands/test-plugin-command.md}
|
|
|
|
# Claude Code lists a plugin's agents and commands with a `readdir` that
|
|
# accepts only regular files, so these must not be per-file symlinks.
|
|
# `assertFileContent` follows symlinks and cannot catch this on its own.
|
|
for componentFile in \
|
|
"$pluginDir/agents/test-plugin-agent.md" \
|
|
"$pluginDir/commands/test-plugin-command.md"; do
|
|
if [[ -L "$componentFile" ]]; then
|
|
echo "Plugin component must be a regular file, not a symlink: $componentFile"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
assertFileContains \
|
|
"$TESTED/home-files/.claude/skills/package-plugin-one/.claude-plugin/plugin.json" \
|
|
'"name":"package-plugin-one"'
|
|
assertFileContains \
|
|
"$TESTED/home-files/.claude/skills/package-plugin-two/.claude-plugin/plugin.json" \
|
|
'"name":"package-plugin-two"'
|
|
assertFileContains \
|
|
"$TESTED/home-files/.claude/skills/manifestless/.claude-plugin/plugin.json" \
|
|
'"name": "manifestless"'
|
|
assertFileContent \
|
|
"$TESTED/home-files/.claude/skills/manifestless/commands/test.md" \
|
|
${./test-command.md}
|
|
'';
|
|
}
|