feat: add global-placement home-manager module (task 0002)
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.
This commit was merged in pull request #2.
This commit is contained in:
154
checks/home-manager-module.nix
Normal file
154
checks/home-manager-module.nix
Normal file
@@ -0,0 +1,154 @@
|
||||
# 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"
|
||||
''
|
||||
Reference in New Issue
Block a user