Files

126 lines
4.2 KiB
Nix

{ lib, pkgs }:
let
jsonFormat = pkgs.formats.json { };
in
{
mkHelpers =
{ configDir }:
let
mkSourceEntry =
content: if lib.hm.strings.isPathLike 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}" ((mkSourceEntry content) // { executable = true; })
) attrs;
mkInstalledMarketplaceEntry =
name: content:
(mkMarketplaceEntry name content)
// {
installLocation = content;
lastUpdated = "1970-01-01T00:00:00Z";
autoUpdate = false;
};
mkMarkdownEntries =
subdir: attrs:
lib.mapAttrs' (
name: content: lib.nameValuePair "${configDir}/${subdir}/${name}.md" (mkSourceEntry content)
) attrs;
# Legacy list entries have no user-supplied name, so derive one from the
# source's base name. Store paths yield unstable names like
# `bxa1s0m3h4sh-source`, which is why the attribute set form is preferred.
derivePluginName =
plugin:
builtins.unsafeDiscardStringContext (
lib.strings.sanitizeDerivationName (baseNameOf (toString plugin))
);
# Wraps a plugin so a manifest can be synthesized for sources that lack
# one. Only top-level entries are linked, leaving each component
# directory a single symlink to the original. `pkgs.symlinkJoin` cannot
# be used here: it mirrors directories and symlinks the leaf files, and
# Claude Code's `agents/` and `commands/` scanners accept only regular
# files, so every agent and command would be dropped.
mkPluginEntry = name: plugin: {
inherit name;
source = pkgs.runCommand "claude-code-${name}" { } ''
mkdir -p "$out"
shopt -s dotglob nullglob
for entry in "${plugin}"/*; do
ln -s "$entry" "$out/$(basename "$entry")"
done
if [[ ! -e $out/.claude-plugin/plugin.json ]]; then
# Replace the linked manifest directory with a real one so the
# generated manifest can sit beside any existing contents.
rm -f "$out/.claude-plugin"
mkdir -p "$out/.claude-plugin"
for entry in "${plugin}"/.claude-plugin/*; do
ln -s "$entry" "$out/.claude-plugin/$(basename "$entry")"
done
install -m644 ${
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.isPath content && lib.pathIsDirectory content then
lib.nameValuePair "${configDir}/skills/${name}" {
source = content;
recursive = true;
}
else if lib.isPath content then
lib.nameValuePair "${configDir}/skills/${name}/SKILL.md" { source = content; }
else if lib.hm.strings.isPathLike content then
lib.nameValuePair "${configDir}/skills/${name}" {
source = pkgs.runCommandLocal "claude-code-skill-${lib.strings.sanitizeDerivationName name}" { } ''
source=${lib.escapeShellArg "${content}"}
if [[ -d "$source" ]]; then
ln -s "$source" "$out"
elif [[ -f "$source" ]]; then
mkdir -p "$out"
ln -s "$source" "$out/SKILL.md"
else
echo "Claude Code skill source '$source' is neither a file nor a directory" >&2
exit 1
fi
'';
recursive = true;
}
else
lib.nameValuePair "${configDir}/skills/${name}/SKILL.md" {
text = content;
};
};
}