opencode: support directory-based configuration for commands, agents, and themes

The `commands`, `agents`, and `themes` options now accept either an
attribute set (existing behavior) or a path to a directory containing
multiple files. When a directory path is provided, it is symlinked to
the appropriate `$XDG_CONFIG_HOME/opencode/` subdirectory.

This change aligns with the existing `skills` option implementation and
provides a more convenient way to manage multiple configuration files
without needing to define each one individually in Nix.
This commit is contained in:
Thierry Delafontaine 2026-01-04 12:59:22 +01:00 committed by Austin Horstman
parent 2be878259a
commit 081234b704
18 changed files with 246 additions and 56 deletions

View file

@ -126,14 +126,22 @@ in
};
commands = lib.mkOption {
type = lib.types.attrsOf (lib.types.either lib.types.lines lib.types.path);
type = lib.types.either (lib.types.attrsOf (lib.types.either lib.types.lines lib.types.path)) lib.types.path;
default = { };
description = ''
Custom commands for opencode.
The attribute name becomes the command filename, and the value is either:
- Inline content as a string
- A path to a file containing the command content
Commands are stored in {file}`$XDG_CONFIG_HOME/opencode/command/` directory.
This option can either be:
- An attribute set defining commands
- A path to a directory containing multiple command files
If an attribute set is used, the attribute name becomes the command filename,
and the value is either:
- Inline content as a string (creates `opencode/command/<name>.md`)
- A path to a file (creates `opencode/command/<name>.md`)
If a path is used, it is expected to contain command files.
The directory is symlinked to {file}`$XDG_CONFIG_HOME/opencode/command/`.
'';
example = lib.literalExpression ''
{
@ -155,14 +163,22 @@ in
};
agents = lib.mkOption {
type = lib.types.attrsOf (lib.types.either lib.types.lines lib.types.path);
type = lib.types.either (lib.types.attrsOf (lib.types.either lib.types.lines lib.types.path)) lib.types.path;
default = { };
description = ''
Custom agents for opencode.
The attribute name becomes the agent filename, and the value is either:
- Inline content as a string
- A path to a file containing the agent content
Agents are stored in {file}`$XDG_CONFIG_HOME/opencode/agent/` directory.
This option can either be:
- An attribute set defining agents
- A path to a directory containing multiple agent files
If an attribute set is used, the attribute name becomes the agent filename,
and the value is either:
- Inline content as a string (creates `opencode/agent/<name>.md`)
- A path to a file (creates `opencode/agent/<name>.md`)
If a path is used, it is expected to contain agent files.
The directory is symlinked to {file}`$XDG_CONFIG_HOME/opencode/agent/`.
'';
example = lib.literalExpression ''
{
@ -227,14 +243,23 @@ in
};
themes = mkOption {
type = lib.types.attrsOf (lib.types.either jsonFormat.type lib.types.path);
type = lib.types.either (lib.types.attrsOf (lib.types.either jsonFormat.type lib.types.path)) lib.types.path;
default = { };
description = ''
Custom themes for opencode. The attribute name becomes the theme
filename, and the value is either:
- An attribute set, that is converted to a json
- A path to a file containing the content
Themes are stored in {file}`$XDG_CONFIG_HOME/opencode/themes/` directory.
Custom themes for opencode.
This option can either be:
- An attribute set defining themes
- A path to a directory containing multiple theme files
If an attribute set is used, the attribute name becomes the theme filename,
and the value is either:
- An attribute set that is converted to a JSON file (creates `opencode/themes/<name>.json`)
- A path to a file (creates `opencode/themes/<name>.json`)
If a path is used, it is expected to contain theme files.
The directory is symlinked to {file}`$XDG_CONFIG_HOME/opencode/themes/`.
Set `programs.opencode.settings.theme` to enable the custom theme.
See <https://opencode.ai/docs/themes/> for the documentation.
'';
@ -243,10 +268,22 @@ in
config = mkIf cfg.enable {
assertions = [
{
assertion = !lib.isPath cfg.commands || lib.pathIsDirectory cfg.commands;
message = "`programs.opencode.commands` must be a directory when set to a path";
}
{
assertion = !lib.isPath cfg.agents || lib.pathIsDirectory cfg.agents;
message = "`programs.opencode.agents` must be a directory when set to a path";
}
{
assertion = !lib.isPath cfg.skills || lib.pathIsDirectory cfg.skills;
message = "`programs.opencode.skills` must be a directory when set to a path";
}
{
assertion = !lib.isPath cfg.themes || lib.pathIsDirectory cfg.themes;
message = "`programs.opencode.themes` must be a directory when set to a path";
}
];
home.packages = mkIf (cfg.package != null) [ cfg.package ];
@ -278,23 +315,42 @@ in
})
);
"opencode/command" = mkIf (lib.isPath cfg.commands) {
source = cfg.commands;
recursive = true;
};
"opencode/agent" = mkIf (lib.isPath cfg.agents) {
source = cfg.agents;
recursive = true;
};
"opencode/skill" = mkIf (lib.isPath cfg.skills) {
source = cfg.skills;
recursive = true;
};
"opencode/themes" = mkIf (lib.isPath cfg.themes) {
source = cfg.themes;
recursive = true;
};
}
// lib.mapAttrs' (
name: content:
lib.nameValuePair "opencode/command/${name}.md" (
if lib.isPath content then { source = content; } else { text = content; }
)
) cfg.commands
// lib.mapAttrs' (
name: content:
lib.nameValuePair "opencode/agent/${name}.md" (
if lib.isPath content then { source = content; } else { text = content; }
)
) cfg.agents
// lib.optionalAttrs (builtins.isAttrs cfg.commands) (
lib.mapAttrs' (
name: content:
lib.nameValuePair "opencode/command/${name}.md" (
if lib.isPath content then { source = content; } else { text = content; }
)
) cfg.commands
)
// lib.optionalAttrs (builtins.isAttrs cfg.agents) (
lib.mapAttrs' (
name: content:
lib.nameValuePair "opencode/agent/${name}.md" (
if lib.isPath content then { source = content; } else { text = content; }
)
) cfg.agents
)
// lib.mapAttrs' (
name: content:
if lib.isPath content && lib.pathIsDirectory content then
@ -307,23 +363,25 @@ in
if lib.isPath content then { source = content; } else { text = content; }
)
) (if builtins.isAttrs cfg.skills then cfg.skills else { })
// lib.mapAttrs' (
name: content:
lib.nameValuePair "opencode/themes/${name}.json" (
if lib.isPath content then
{
source = content;
}
else
{
source = jsonFormat.generate "opencode-${name}.json" (
{
"$schema" = "https://opencode.ai/theme.json";
}
// content
);
}
)
) cfg.themes;
// lib.optionalAttrs (builtins.isAttrs cfg.themes) (
lib.mapAttrs' (
name: content:
lib.nameValuePair "opencode/themes/${name}.json" (
if lib.isPath content then
{
source = content;
}
else
{
source = jsonFormat.generate "opencode-${name}.json" (
{
"$schema" = "https://opencode.ai/theme.json";
}
// content
);
}
)
) cfg.themes
);
};
}