mirror of
https://github.com/nix-community/home-manager.git
synced 2026-06-05 21:02:51 +00:00
treewide: support store path strings for skills dirs
Filtered or generated skill directories can evaluate to Nix store path strings rather than path values. The previous top-level skills handling only used lib.isPath, so those directory values fell through instead of being linked or expanded. Treat store-path strings as path-like for the top-level skills option in Codex, Claude Code, Gemini CLI, and OpenCode, matching the behavior already used for individual skill entries. Add NMT coverage for store-path-string skill directories.
This commit is contained in:
@@ -514,7 +514,8 @@ in
|
||||
let
|
||||
mkSourceEntry = content: if lib.isPath content then { source = content; } else { text = content; };
|
||||
|
||||
isStorePathString = content: builtins.isString content && lib.hasPrefix builtins.storeDir content;
|
||||
isStorePathString =
|
||||
content: builtins.isString content && lib.hasPrefix "${builtins.storeDir}/" content;
|
||||
isPathLikeContent = content: lib.isPath content || isStorePathString content;
|
||||
|
||||
mkMarkdownEntries =
|
||||
@@ -587,7 +588,7 @@ in
|
||||
message = "`programs.claude-code.package` cannot be null when `mcpServers`, `lspServers`, `enableMcpIntegration`, or `plugins` is configured";
|
||||
}
|
||||
{
|
||||
assertion = !lib.isPath cfg.skills || lib.pathIsDirectory cfg.skills;
|
||||
assertion = !isPathLikeContent cfg.skills || lib.pathIsDirectory cfg.skills;
|
||||
message = "`programs.claude-code.skills` must be a directory when set to a path";
|
||||
}
|
||||
]
|
||||
@@ -680,7 +681,7 @@ in
|
||||
(mkRecursiveDirAttrs "commands" cfg.commandsDir)
|
||||
(mkRecursiveDirAttrs "hooks" cfg.hooksDir)
|
||||
(mkRecursiveDirAttrs "rules" cfg.rulesDir)
|
||||
(lib.mkIf (lib.isPath cfg.skills) {
|
||||
(lib.mkIf (isPathLikeContent cfg.skills) {
|
||||
".claude/skills" = {
|
||||
source = cfg.skills;
|
||||
recursive = true;
|
||||
|
||||
@@ -191,7 +191,8 @@ in
|
||||
# TODO: Remove this workaround once Codex supports symlinked SKILL.md
|
||||
# files again. Upstream only supports symlinking the containing skill
|
||||
# directory today: https://github.com/openai/codex/issues/10470
|
||||
isStorePathString = content: builtins.isString content && lib.hasPrefix builtins.storeDir content;
|
||||
isStorePathString =
|
||||
content: builtins.isString content && lib.hasPrefix "${builtins.storeDir}/" content;
|
||||
isPathLikeContent = content: lib.isPath content || isStorePathString content;
|
||||
mkSkillDir =
|
||||
content:
|
||||
@@ -201,7 +202,7 @@ in
|
||||
skillSources =
|
||||
if builtins.isAttrs cfg.skills then
|
||||
cfg.skills
|
||||
else if lib.isPath cfg.skills && lib.pathIsDirectory cfg.skills then
|
||||
else if isPathLikeContent cfg.skills && lib.pathIsDirectory cfg.skills then
|
||||
lib.mapAttrs (name: _type: cfg.skills + "/${name}") (builtins.readDir cfg.skills)
|
||||
else
|
||||
{ };
|
||||
@@ -250,7 +251,7 @@ in
|
||||
mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = !lib.isPath cfg.skills || lib.pathIsDirectory cfg.skills;
|
||||
assertion = !isPathLikeContent cfg.skills || lib.pathIsDirectory cfg.skills;
|
||||
message = "`programs.codex.skills` must be a directory when set to a path";
|
||||
}
|
||||
{
|
||||
|
||||
@@ -227,82 +227,88 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable (
|
||||
lib.mkMerge [
|
||||
{
|
||||
programs.gemini-cli.settings.mcpServers = lib.mkIf (
|
||||
cfg.enableMcpIntegration && config.programs.mcp.enable
|
||||
) (lib.mapAttrs (_n: lib.mkDefault) config.programs.mcp.servers);
|
||||
}
|
||||
{
|
||||
home = {
|
||||
packages = lib.mkIf (cfg.package != null) [ cfg.package ];
|
||||
file.".gemini/settings.json" = lib.mkIf (cfg.settings != { }) {
|
||||
source = jsonFormat.generate "gemini-cli-settings.json" cfg.settings;
|
||||
};
|
||||
sessionVariables = lib.mkIf (cfg.defaultModel != null) {
|
||||
GEMINI_MODEL = cfg.defaultModel;
|
||||
};
|
||||
};
|
||||
}
|
||||
{
|
||||
home.file = lib.mapAttrs' (
|
||||
n: v: lib.nameValuePair ".gemini/${n}.md" (if lib.isPath v then { source = v; } else { text = v; })
|
||||
) cfg.context;
|
||||
}
|
||||
{
|
||||
home.file = lib.mapAttrs' (
|
||||
n: v:
|
||||
lib.nameValuePair ".gemini/commands/${n}.toml" {
|
||||
source = tomlFormat.generate "gemini-cli-command-${n}.toml" {
|
||||
inherit (v) description prompt;
|
||||
config =
|
||||
let
|
||||
isStorePathString =
|
||||
content: builtins.isString content && lib.hasPrefix "${builtins.storeDir}/" content;
|
||||
isPathLikeContent = content: lib.isPath content || isStorePathString content;
|
||||
in
|
||||
lib.mkIf cfg.enable (
|
||||
lib.mkMerge [
|
||||
{
|
||||
programs.gemini-cli.settings.mcpServers = lib.mkIf (
|
||||
cfg.enableMcpIntegration && config.programs.mcp.enable
|
||||
) (lib.mapAttrs (_n: lib.mkDefault) config.programs.mcp.servers);
|
||||
}
|
||||
{
|
||||
home = {
|
||||
packages = lib.mkIf (cfg.package != null) [ cfg.package ];
|
||||
file.".gemini/settings.json" = lib.mkIf (cfg.settings != { }) {
|
||||
source = jsonFormat.generate "gemini-cli-settings.json" cfg.settings;
|
||||
};
|
||||
}
|
||||
) cfg.commands;
|
||||
}
|
||||
{
|
||||
home.file = lib.mapAttrs' (
|
||||
n: v:
|
||||
lib.nameValuePair ".gemini/policies/${n}.toml" {
|
||||
source =
|
||||
if builtins.isPath v || builtins.isString v || lib.isDerivation v then
|
||||
v
|
||||
else
|
||||
tomlFormat.generate "gemini-cli-policy-${n}.toml" v;
|
||||
}
|
||||
) cfg.policies;
|
||||
}
|
||||
{
|
||||
assertions = [
|
||||
{
|
||||
assertion = !lib.isPath cfg.skills || lib.pathIsDirectory cfg.skills;
|
||||
message = "`programs.gemini-cli.skills` must be a directory when set to a path";
|
||||
}
|
||||
];
|
||||
}
|
||||
{
|
||||
home.file =
|
||||
if lib.isPath cfg.skills then
|
||||
{
|
||||
".gemini/skills" = {
|
||||
source = cfg.skills;
|
||||
recursive = true;
|
||||
sessionVariables = lib.mkIf (cfg.defaultModel != null) {
|
||||
GEMINI_MODEL = cfg.defaultModel;
|
||||
};
|
||||
};
|
||||
}
|
||||
{
|
||||
home.file = lib.mapAttrs' (
|
||||
n: v: lib.nameValuePair ".gemini/${n}.md" (if lib.isPath v then { source = v; } else { text = v; })
|
||||
) cfg.context;
|
||||
}
|
||||
{
|
||||
home.file = lib.mapAttrs' (
|
||||
n: v:
|
||||
lib.nameValuePair ".gemini/commands/${n}.toml" {
|
||||
source = tomlFormat.generate "gemini-cli-command-${n}.toml" {
|
||||
inherit (v) description prompt;
|
||||
};
|
||||
}
|
||||
else
|
||||
lib.mapAttrs' (
|
||||
n: v:
|
||||
if lib.isPath v && lib.pathIsDirectory v then
|
||||
lib.nameValuePair ".gemini/skills/${n}" {
|
||||
source = v;
|
||||
) cfg.commands;
|
||||
}
|
||||
{
|
||||
home.file = lib.mapAttrs' (
|
||||
n: v:
|
||||
lib.nameValuePair ".gemini/policies/${n}.toml" {
|
||||
source =
|
||||
if builtins.isPath v || builtins.isString v || lib.isDerivation v then
|
||||
v
|
||||
else
|
||||
tomlFormat.generate "gemini-cli-policy-${n}.toml" v;
|
||||
}
|
||||
) cfg.policies;
|
||||
}
|
||||
{
|
||||
assertions = [
|
||||
{
|
||||
assertion = !isPathLikeContent cfg.skills || lib.pathIsDirectory cfg.skills;
|
||||
message = "`programs.gemini-cli.skills` must be a directory when set to a path";
|
||||
}
|
||||
];
|
||||
}
|
||||
{
|
||||
home.file =
|
||||
if isPathLikeContent cfg.skills then
|
||||
{
|
||||
".gemini/skills" = {
|
||||
source = cfg.skills;
|
||||
recursive = true;
|
||||
}
|
||||
else
|
||||
lib.nameValuePair ".gemini/skills/${n}/SKILL.md" (
|
||||
if lib.isPath v then { source = v; } else { text = v; }
|
||||
)
|
||||
) cfg.skills;
|
||||
}
|
||||
]
|
||||
);
|
||||
};
|
||||
}
|
||||
else
|
||||
lib.mapAttrs' (
|
||||
n: v:
|
||||
if isPathLikeContent v && lib.pathIsDirectory v then
|
||||
lib.nameValuePair ".gemini/skills/${n}" {
|
||||
source = v;
|
||||
recursive = true;
|
||||
}
|
||||
else
|
||||
lib.nameValuePair ".gemini/skills/${n}/SKILL.md" (
|
||||
if isPathLikeContent v then { source = v; } else { text = v; }
|
||||
)
|
||||
) cfg.skills;
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,6 +62,10 @@ let
|
||||
}
|
||||
else
|
||||
cfg.package;
|
||||
|
||||
isStorePathString =
|
||||
content: builtins.isString content && lib.hasPrefix "${builtins.storeDir}/" content;
|
||||
isPathLikeContent = content: lib.isPath content || isStorePathString content;
|
||||
in
|
||||
{
|
||||
meta.maintainers = with lib.maintainers; [ delafthi ];
|
||||
@@ -435,7 +439,7 @@ in
|
||||
message = "`programs.opencode.tools` must be a directory when set to a path";
|
||||
}
|
||||
{
|
||||
assertion = !lib.isPath cfg.skills || lib.pathIsDirectory cfg.skills;
|
||||
assertion = !isPathLikeContent cfg.skills || lib.pathIsDirectory cfg.skills;
|
||||
message = "`programs.opencode.skills` must be a directory when set to a path";
|
||||
}
|
||||
{
|
||||
@@ -522,7 +526,7 @@ in
|
||||
recursive = true;
|
||||
};
|
||||
|
||||
"opencode/skills" = mkIf (lib.isPath cfg.skills) {
|
||||
"opencode/skills" = mkIf (isPathLikeContent cfg.skills) {
|
||||
source = cfg.skills;
|
||||
recursive = true;
|
||||
};
|
||||
@@ -558,17 +562,14 @@ in
|
||||
)
|
||||
// lib.mapAttrs' (
|
||||
name: content:
|
||||
if
|
||||
(lib.isPath content && lib.pathIsDirectory content)
|
||||
|| (builtins.isString content && lib.hasPrefix builtins.storeDir content)
|
||||
then
|
||||
if isPathLikeContent content && lib.pathIsDirectory content then
|
||||
lib.nameValuePair "opencode/skills/${name}" {
|
||||
source = content;
|
||||
recursive = true;
|
||||
}
|
||||
else
|
||||
lib.nameValuePair "opencode/skills/${name}/SKILL.md" (
|
||||
if lib.isPath content then { source = content; } else { text = content; }
|
||||
if isPathLikeContent content then { source = content; } else { text = content; }
|
||||
)
|
||||
) (if builtins.isAttrs cfg.skills then cfg.skills else { })
|
||||
// lib.optionalAttrs (builtins.isAttrs cfg.themes) (
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
claude-code-commands-dir = ./commands-dir.nix;
|
||||
claude-code-hooks-dir = ./hooks-dir.nix;
|
||||
claude-code-skills-dir = ./skills-dir.nix;
|
||||
claude-code-skills-store-path-dir = ./skills-store-path-dir.nix;
|
||||
claude-code-skills-subdir = ./skills-subdir.nix;
|
||||
claude-code-agents-path = ./agents-path.nix;
|
||||
claude-code-commands-path = ./commands-path.nix;
|
||||
|
||||
20
tests/modules/programs/claude-code/skills-store-path-dir.nix
Normal file
20
tests/modules/programs/claude-code/skills-store-path-dir.nix
Normal file
@@ -0,0 +1,20 @@
|
||||
{ pkgs, ... }:
|
||||
|
||||
let
|
||||
src = pkgs.writeTextDir "skills/external-skill/SKILL.md" ''
|
||||
# External Skill
|
||||
'';
|
||||
in
|
||||
{
|
||||
programs.claude-code = {
|
||||
enable = true;
|
||||
skills = "${src}/skills";
|
||||
};
|
||||
|
||||
nmt.script = ''
|
||||
assertFileExists home-files/.claude/skills/external-skill/SKILL.md
|
||||
assertLinkExists home-files/.claude/skills/external-skill/SKILL.md
|
||||
assertFileContent home-files/.claude/skills/external-skill/SKILL.md \
|
||||
"${src}/skills/external-skill/SKILL.md"
|
||||
'';
|
||||
}
|
||||
@@ -12,5 +12,6 @@
|
||||
codex-skills-inline-legacy-path = ./skills-inline-legacy-path.nix;
|
||||
codex-skills-dir = ./skills-dir.nix;
|
||||
codex-skills-store-path = ./skills-store-path.nix;
|
||||
codex-skills-store-path-dir = ./skills-store-path-dir.nix;
|
||||
codex-skills-path-not-directory = ./skills-path-not-directory.nix;
|
||||
}
|
||||
|
||||
25
tests/modules/programs/codex/skills-store-path-dir.nix
Normal file
25
tests/modules/programs/codex/skills-store-path-dir.nix
Normal file
@@ -0,0 +1,25 @@
|
||||
{ pkgs, ... }:
|
||||
|
||||
let
|
||||
src = pkgs.writeTextDir "skills/external-skill/SKILL.md" ''
|
||||
---
|
||||
name: external-skill
|
||||
description: Store path skill directory fixture.
|
||||
---
|
||||
|
||||
# External Skill
|
||||
'';
|
||||
in
|
||||
{
|
||||
programs.codex = {
|
||||
enable = true;
|
||||
skills = "${src}/skills";
|
||||
};
|
||||
|
||||
nmt.script = ''
|
||||
assertLinkExists home-files/.codex/skills/external-skill
|
||||
assertFileExists home-files/.codex/skills/external-skill/SKILL.md
|
||||
assertFileContent home-files/.codex/skills/external-skill/SKILL.md \
|
||||
"${src}/skills/external-skill/SKILL.md"
|
||||
'';
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
gemini-cli-context = ./context.nix;
|
||||
gemini-cli-skills = ./skills.nix;
|
||||
gemini-cli-skills-dir = ./skills-dir.nix;
|
||||
gemini-cli-skills-store-path-dir = ./skills-store-path-dir.nix;
|
||||
gemini-cli-skills-path-not-directory = ./skills-path-not-directory.nix;
|
||||
gemini-cli-mcp-servers = ./mcp.nix;
|
||||
}
|
||||
|
||||
20
tests/modules/programs/gemini-cli/skills-store-path-dir.nix
Normal file
20
tests/modules/programs/gemini-cli/skills-store-path-dir.nix
Normal file
@@ -0,0 +1,20 @@
|
||||
{ pkgs, ... }:
|
||||
|
||||
let
|
||||
src = pkgs.writeTextDir "skills/external-skill/SKILL.md" ''
|
||||
# External Skill
|
||||
'';
|
||||
in
|
||||
{
|
||||
programs.gemini-cli = {
|
||||
enable = true;
|
||||
skills = "${src}/skills";
|
||||
};
|
||||
|
||||
nmt.script = ''
|
||||
assertFileExists home-files/.gemini/skills/external-skill/SKILL.md
|
||||
assertLinkExists home-files/.gemini/skills/external-skill/SKILL.md
|
||||
assertFileContent home-files/.gemini/skills/external-skill/SKILL.md \
|
||||
"${src}/skills/external-skill/SKILL.md"
|
||||
'';
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
opencode-skills-inline = ./skills-inline.nix;
|
||||
opencode-skills-path = ./skills-path.nix;
|
||||
opencode-skills-store-path = ./skills-store-path.nix;
|
||||
opencode-skills-store-path-dir = ./skills-store-path-dir.nix;
|
||||
opencode-skills-directory = ./skills-directory.nix;
|
||||
opencode-skills-bulk-directory = ./skills-bulk-directory.nix;
|
||||
opencode-themes-inline = ./themes-inline.nix;
|
||||
|
||||
20
tests/modules/programs/opencode/skills-store-path-dir.nix
Normal file
20
tests/modules/programs/opencode/skills-store-path-dir.nix
Normal file
@@ -0,0 +1,20 @@
|
||||
{ pkgs, ... }:
|
||||
|
||||
let
|
||||
src = pkgs.writeTextDir "skills/external-skill/SKILL.md" ''
|
||||
# External Skill
|
||||
'';
|
||||
in
|
||||
{
|
||||
programs.opencode = {
|
||||
enable = true;
|
||||
skills = "${src}/skills";
|
||||
};
|
||||
|
||||
nmt.script = ''
|
||||
assertFileExists home-files/.config/opencode/skills/external-skill/SKILL.md
|
||||
assertLinkExists home-files/.config/opencode/skills/external-skill/SKILL.md
|
||||
assertFileContent home-files/.config/opencode/skills/external-skill/SKILL.md \
|
||||
"${src}/skills/external-skill/SKILL.md"
|
||||
'';
|
||||
}
|
||||
Reference in New Issue
Block a user