Expose homeModules.default, a home-manager module whose programs.agents.skills option places operator-selected skill derivations into Claude Code's skills directory, one recursive home.file per skill, gated on programs.claude-code.enable and sourcing configDir from the claude-code module. Add a nix flake check that builds the home-files tree under several operator configurations and asserts placement, recursion, the enable gate, configDir sourcing, and coexistence with an operator's own skills.
155 lines
5.7 KiB
Nix
155 lines
5.7 KiB
Nix
# Builds the home-files tree `homeModules.default` produces under several
|
|
# operator configurations and asserts on the skills each one places.
|
|
{
|
|
pkgs,
|
|
home-manager,
|
|
module,
|
|
mkSkill,
|
|
}:
|
|
let
|
|
inherit (pkgs) lib;
|
|
|
|
# A sample skill built through the flake's own `mkSkill`, so it carries the
|
|
# `skillName` passthru the module reads to place it.
|
|
mkSampleSkill =
|
|
name:
|
|
mkSkill {
|
|
inherit pkgs name;
|
|
src = pkgs.runCommandLocal "${name}-src" { } ''
|
|
mkdir -p "$out"
|
|
printf '%s\n' "sample skill ${name}" > "$out/SKILL.md"
|
|
'';
|
|
};
|
|
|
|
skillAlpha = mkSampleSkill "sample-alpha";
|
|
skillBeta = mkSampleSkill "sample-beta";
|
|
|
|
# A skill the operator declares as their own attribute-set entry, so the
|
|
# attribute-set assertion can look for it beside the module's.
|
|
operatorAttrSkill = pkgs.runCommandLocal "operator-attr-skill" { } ''
|
|
mkdir -p "$out"
|
|
printf '%s\n' "the operator's own attribute-set skill" > "$out/SKILL.md"
|
|
'';
|
|
|
|
# A whole directory of skills, one folder per skill, for the path form of the
|
|
# operator's own `programs.claude-code.skills`.
|
|
operatorSkillsDir = pkgs.runCommandLocal "operator-skills-dir" { } ''
|
|
mkdir -p "$out/operator-dir-skill"
|
|
printf '%s\n' "the operator's own whole-directory skill" \
|
|
> "$out/operator-dir-skill/SKILL.md"
|
|
'';
|
|
|
|
# Evaluate the real module through home-manager's standalone entry point and
|
|
# return the home-files tree it would link into $HOME.
|
|
homeFiles =
|
|
operatorConfig:
|
|
(home-manager.lib.homeManagerConfiguration {
|
|
inherit pkgs;
|
|
modules = [
|
|
module
|
|
{
|
|
home.username = "operator";
|
|
home.homeDirectory = "/home/operator";
|
|
home.stateVersion = "24.11";
|
|
}
|
|
operatorConfig
|
|
];
|
|
}).config.home-files;
|
|
|
|
# Two skills selected beside the operator's own attribute-set skill, with
|
|
# Claude Code enabled.
|
|
attrSetSkills = homeFiles {
|
|
programs.claude-code.enable = true;
|
|
programs.agents.skills = [
|
|
skillAlpha
|
|
skillBeta
|
|
];
|
|
# A store path, not a bare derivation, so the value takes the option's path
|
|
# branch rather than its attribute-set branch.
|
|
programs.claude-code.skills.operator-attr-skill = "${operatorAttrSkill}";
|
|
};
|
|
|
|
# The operator's own skills as a whole directory (path form).
|
|
# Setting this single-valued option to a path also proves the module never
|
|
# contributes to it, since an attribute-set contribution would clash with the
|
|
# path at eval.
|
|
wholeDirSkills = homeFiles {
|
|
programs.claude-code.enable = true;
|
|
programs.agents.skills = [ skillAlpha ];
|
|
programs.claude-code.skills = "${operatorSkillsDir}";
|
|
};
|
|
|
|
# A non-default `configDir`, so the assertions can prove placement follows it
|
|
# rather than a hardcoded `.claude`.
|
|
customConfigDir = homeFiles {
|
|
programs.claude-code.enable = true;
|
|
programs.claude-code.configDir = "/home/operator/.config/claude";
|
|
programs.agents.skills = [ skillAlpha ];
|
|
};
|
|
|
|
# Claude Code disabled, with a skill still selected.
|
|
claudeCodeOff = homeFiles {
|
|
programs.claude-code.enable = false;
|
|
programs.agents.skills = [ skillAlpha ];
|
|
};
|
|
|
|
# An empty selection with Claude Code enabled.
|
|
emptySelection = homeFiles {
|
|
programs.claude-code.enable = true;
|
|
programs.agents.skills = [ ];
|
|
};
|
|
in
|
|
pkgs.runCommandLocal "skills-home-manager-module-check"
|
|
{
|
|
# Forcing each derivation as a build input builds its home-files tree, so an
|
|
# eval-level composition failure surfaces before any assertion runs.
|
|
inherit
|
|
attrSetSkills
|
|
wholeDirSkills
|
|
customConfigDir
|
|
claudeCodeOff
|
|
emptySelection
|
|
;
|
|
}
|
|
''
|
|
fail() { echo "FAIL: $1" >&2; exit 1; }
|
|
|
|
echo "attribute-set skills: both selected skills and the operator's own land"
|
|
test -f "$attrSetSkills/.claude/skills/sample-alpha/SKILL.md" \
|
|
|| fail "selected skill sample-alpha did not land at its own name"
|
|
test -f "$attrSetSkills/.claude/skills/sample-beta/SKILL.md" \
|
|
|| fail "selected skill sample-beta did not land at its own name"
|
|
test -f "$attrSetSkills/.claude/skills/operator-attr-skill/SKILL.md" \
|
|
|| fail "the operator's own attribute-set skill was clobbered"
|
|
|
|
echo "recursive placement: a skill is a real directory of per-file symlinks"
|
|
# A file-existence test follows symlinks, so it cannot tell a recursive tree
|
|
# from one opaque symlink over the whole skill.
|
|
# The entry's own type is what the assertion reads instead.
|
|
test -d "$attrSetSkills/.claude/skills/sample-alpha" \
|
|
&& test ! -L "$attrSetSkills/.claude/skills/sample-alpha" \
|
|
|| fail "sample-alpha was placed as an opaque symlink, not a recursive tree"
|
|
|
|
echo "whole-directory skills: the selected skill lands beside the operator's directory"
|
|
test -f "$wholeDirSkills/.claude/skills/sample-alpha/SKILL.md" \
|
|
|| fail "selected skill did not coexist with a whole skills directory"
|
|
test -f "$wholeDirSkills/.claude/skills/operator-dir-skill/SKILL.md" \
|
|
|| fail "the operator's whole skills directory was clobbered"
|
|
|
|
echo "custom configDir: placement follows configDir rather than a hardcoded .claude"
|
|
test -f "$customConfigDir/.config/claude/skills/sample-alpha/SKILL.md" \
|
|
|| fail "skill was not placed under the configured configDir"
|
|
test ! -e "$customConfigDir/.claude/skills/sample-alpha" \
|
|
|| fail "skill was placed under a hardcoded .claude, ignoring configDir"
|
|
|
|
echo "Claude Code disabled: no skill entry is written despite a selection"
|
|
test ! -e "$claudeCodeOff/.claude/skills/sample-alpha" \
|
|
|| fail "a skill was placed with claude-code disabled"
|
|
|
|
echo "empty selection: nothing is installed"
|
|
test ! -e "$emptySelection/.claude/skills/sample-alpha" \
|
|
|| fail "an unselected skill was placed"
|
|
|
|
touch "$out"
|
|
''
|