mirror of
https://github.com/nix-community/home-manager.git
synced 2026-10-30 16:10:45 +00:00
Add programs.opencode.cli for OpenCode 2's global cli.json, while retaining programs.opencode.tui for OpenCode 1. Support mutable CLI configuration through OPENCODE_CLI_CONFIG_CONTENT, leaving cli.json writable by OpenCode. Add validation against the configured package's CLI schema and warn when CLI or TUI settings do not match the package version.
934 lines
32 KiB
Nix
934 lines
32 KiB
Nix
{
|
|
lib,
|
|
pkgs,
|
|
config,
|
|
...
|
|
}:
|
|
let
|
|
inherit (lib)
|
|
literalExpression
|
|
mkEnableOption
|
|
mkIf
|
|
mkOption
|
|
mkPackageOption
|
|
;
|
|
|
|
cfg = config.programs.opencode;
|
|
webCfg = cfg.web;
|
|
hasSettings = cfg.settings != { } || transformedMcpServers != { };
|
|
hasCli = cfg.cli != { };
|
|
settingsPath = "opencode/${if cfg.mutableSettings then "home-manager.json" else "opencode.json"}";
|
|
tuiPath = "opencode/${if cfg.mutableSettings then "home-manager-tui.json" else "tui.json"}";
|
|
cliPath = "opencode/${if cfg.mutableSettings then "home-manager-cli.json" else "cli.json"}";
|
|
effectiveFile = path: config.home.file."${config.xdg.configHome}/${path}";
|
|
absoluteTarget =
|
|
path:
|
|
let
|
|
inherit ((effectiveFile path)) target;
|
|
in
|
|
if lib.hasPrefix "/" target then target else "${config.home.homeDirectory}/${target}";
|
|
mutableEnvironment = lib.optionalAttrs cfg.mutableSettings (
|
|
lib.optionalAttrs (hasSettings && (effectiveFile settingsPath).enable) {
|
|
OPENCODE_CONFIG = absoluteTarget settingsPath;
|
|
}
|
|
// lib.optionalAttrs (cfg.tui != { } && (effectiveFile tuiPath).enable) {
|
|
OPENCODE_TUI_CONFIG = absoluteTarget tuiPath;
|
|
}
|
|
// lib.optionalAttrs (hasCli && (effectiveFile cliPath).enable) {
|
|
OPENCODE_CLI_CONFIG_CONTENT = cliJson;
|
|
}
|
|
);
|
|
|
|
jsonFormat = pkgs.formats.json { };
|
|
cliConfig = {
|
|
"$schema" = "https://opencode.ai/v2/cli.json";
|
|
}
|
|
// cfg.cli;
|
|
cliJson = builtins.toJSON cliConfig;
|
|
|
|
packageVersion = if cfg.package != null then lib.getVersion cfg.package else null;
|
|
isOpenCode2 = packageVersion == null || lib.versionAtLeast packageVersion "2.0.0";
|
|
packageSchema =
|
|
name: if cfg.package == null then null else cfg.package.passthru.jsonschema.${name} or null;
|
|
configSchema = packageSchema "config";
|
|
tuiSchema = packageSchema "tui";
|
|
cliSchema = packageSchema "cli";
|
|
|
|
toOpencodeShape =
|
|
s:
|
|
let
|
|
isRemote = s ? url && s.url != null;
|
|
renderedEnv = lib.hm.mcp.renderEnv (p: "{file:${p}}") (s.env or { });
|
|
in
|
|
lib.optionalAttrs (s.enabled or null != null) { inherit (s) enabled; }
|
|
// {
|
|
type = if isRemote then "remote" else "local";
|
|
}
|
|
// (
|
|
if isRemote then
|
|
{ inherit (s) url; } // lib.optionalAttrs (s.headers or { } != { }) { inherit (s) headers; }
|
|
else
|
|
{
|
|
command = [ s.command ] ++ (s.args or [ ]);
|
|
}
|
|
// lib.optionalAttrs (renderedEnv != { }) { environment = renderedEnv; }
|
|
);
|
|
|
|
transformedMcpServers =
|
|
if cfg.enableMcpIntegration && config.programs.mcp.enable && config.programs.mcp.servers != { } then
|
|
lib.mapAttrs (
|
|
_: server:
|
|
lib.hm.mcp.transformMcpServer {
|
|
inherit server;
|
|
extraTransforms = [ toOpencodeShape ];
|
|
exclude = [
|
|
"args"
|
|
"env"
|
|
];
|
|
}
|
|
) config.programs.mcp.servers
|
|
else
|
|
{ };
|
|
|
|
packageWithExtraPackages =
|
|
if cfg.package != null && cfg.extraPackages != [ ] then
|
|
pkgs.symlinkJoin {
|
|
inherit (cfg.package) meta;
|
|
name = "${lib.getName cfg.package}-wrapped-${lib.getVersion cfg.package}";
|
|
paths = [ cfg.package ];
|
|
preferLocalBuild = true;
|
|
nativeBuildInputs = [ pkgs.makeWrapper ];
|
|
postBuild = ''
|
|
wrapProgram $out/bin/${cfg.package.meta.mainProgram} \
|
|
--suffix PATH : ${lib.makeBinPath cfg.extraPackages}
|
|
'';
|
|
}
|
|
else
|
|
cfg.package;
|
|
|
|
normalizeDirectory =
|
|
name: source:
|
|
if lib.isPath source then
|
|
source
|
|
else
|
|
pkgs.runCommandLocal name { } ''
|
|
if [[ ! -d ${lib.escapeShellArg (toString source)} ]]; then
|
|
echo ${lib.escapeShellArg "programs.opencode.skills must be a directory"} >&2
|
|
exit 1
|
|
fi
|
|
ln -s ${lib.escapeShellArg (toString source)} "$out"
|
|
'';
|
|
|
|
normalizeSkill =
|
|
source:
|
|
pkgs.runCommandLocal "opencode-skill" { } ''
|
|
source=${lib.escapeShellArg (toString source)}
|
|
if [[ -d "$source" ]]; then
|
|
ln -s "$source" "$out"
|
|
elif [[ -f "$source" ]]; then
|
|
mkdir "$out"
|
|
ln -s "$source" "$out/SKILL.md"
|
|
else
|
|
echo "OpenCode skill source must be a file or directory: $source" >&2
|
|
exit 1
|
|
fi
|
|
'';
|
|
|
|
webProgramArguments = [
|
|
(lib.getExe packageWithExtraPackages)
|
|
"serve"
|
|
]
|
|
++ webCfg.extraArgs;
|
|
|
|
opencodeWebLauncher = pkgs.writeShellScriptBin "opencode-web-launcher" ''
|
|
${lib.concatStringsSep "\n" (
|
|
lib.mapAttrsToList (name: value: "export ${name}=${lib.escapeShellArg value}") mutableEnvironment
|
|
)}
|
|
export PATH="${config.home.profileDirectory}/bin''${PATH:+:$PATH}"
|
|
exec ${lib.escapeShellArgs webProgramArguments}
|
|
'';
|
|
in
|
|
{
|
|
meta.maintainers = with lib.maintainers; [ delafthi ];
|
|
|
|
imports = [
|
|
(lib.mkRenamedOptionModule [ "programs" "opencode" "rules" ] [ "programs" "opencode" "context" ])
|
|
];
|
|
|
|
options.programs.opencode = {
|
|
enable = mkEnableOption "opencode";
|
|
|
|
package = mkPackageOption pkgs "opencode" { nullable = true; };
|
|
|
|
mutableSettings = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
example = true;
|
|
description = ''
|
|
Whether to leave {file}`$XDG_CONFIG_HOME/opencode/opencode.json`,
|
|
{file}`$XDG_CONFIG_HOME/opencode/cli.json`, and
|
|
{file}`$XDG_CONFIG_HOME/opencode/tui.json` writable by OpenCode.
|
|
|
|
When enabled, declarative settings, CLI settings, and TUI settings are
|
|
written to {file}`$XDG_CONFIG_HOME/opencode/home-manager.json`,
|
|
{file}`$XDG_CONFIG_HOME/opencode/home-manager-cli.json`, and
|
|
{file}`$XDG_CONFIG_HOME/opencode/home-manager-tui.json`. Declarative
|
|
settings and TUI settings are loaded through {env}`OPENCODE_CONFIG` and
|
|
{env}`OPENCODE_TUI_CONFIG`. CLI settings are loaded through
|
|
{env}`OPENCODE_CLI_CONFIG_CONTENT` because OpenCode 2 does not provide a
|
|
path selector for {file}`cli.json`; the generated
|
|
{file}`home-manager-cli.json` layer contains the same content for
|
|
inspection and validation. Declarative settings override global user
|
|
settings; project settings can still override them.
|
|
{env}`OPENCODE_TUI_CONFIG` requires OpenCode 1.2.15 or later and is
|
|
ignored by OpenCode 2. {env}`OPENCODE_CLI_CONFIG_CONTENT` requires
|
|
OpenCode 2.
|
|
|
|
These variables are set through {option}`home.sessionVariables` and must
|
|
reach OpenCode launches outside the web service for declarative settings
|
|
to be loaded. The web service receives them automatically.
|
|
|
|
Disabling this option restores Home Manager management of the main files.
|
|
If OpenCode has created those files, activation reports the normal file
|
|
collision; back up or move the app-owned files before switching back.
|
|
OpenCode can also create {file}`$XDG_CONFIG_HOME/opencode/opencode.jsonc`,
|
|
which takes precedence over the managed {file}`opencode.json` without
|
|
causing a collision. Back up and move that file, or reconcile its
|
|
contents with your declarations, before disabling this option.
|
|
'';
|
|
};
|
|
|
|
extraPackages = mkOption {
|
|
type = with lib.types; listOf package;
|
|
default = [ ];
|
|
example = literalExpression "[ pkgs.uv ]";
|
|
description = "Extra packages available to OpenCode.";
|
|
};
|
|
|
|
enableMcpIntegration = mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = ''
|
|
Whether to integrate the MCP servers config from
|
|
{option}`programs.mcp.servers` into
|
|
{option}`programs.opencode.settings.mcp`.
|
|
|
|
Note: Settings defined in {option}`programs.mcp.servers` are merged
|
|
with {option}`programs.opencode.settings.mcp`, with OpenCode settings
|
|
taking precedence.
|
|
'';
|
|
};
|
|
|
|
settings = mkOption {
|
|
inherit (jsonFormat) type;
|
|
default = { };
|
|
example = {
|
|
model = "anthropic/claude-sonnet-4-20250514";
|
|
autoshare = false;
|
|
autoupdate = true;
|
|
};
|
|
description = ''
|
|
Configuration written to {file}`$XDG_CONFIG_HOME/opencode/opencode.json`.
|
|
See <https://opencode.ai/docs/config/> for the documentation.
|
|
|
|
Attribute sets containing ordered `lib.hm.dag.entryBefore` or
|
|
`lib.hm.dag.entryAfter` values are rendered in topological order, with
|
|
raw sibling values treated as unordered entries. This is useful for
|
|
OpenCode permission rules, where the last matching rule wins.
|
|
|
|
The `plugin` key accepts a list of plugin references: local paths to
|
|
plugin directories or files, derivations (e.g. `fetchFromGitHub`),
|
|
string paths into derivations, or names of external plugins fetched and
|
|
built by OpenCode.
|
|
|
|
Note, `"$schema": "https://opencode.ai/config.json"` is automatically added to the configuration.
|
|
'';
|
|
};
|
|
|
|
tui = mkOption {
|
|
inherit (jsonFormat) type;
|
|
default = { };
|
|
example = {
|
|
theme = "system";
|
|
keybinds = {
|
|
leader = "alt+b";
|
|
};
|
|
};
|
|
|
|
description = ''
|
|
TUI-specific configuration written to {file}`$XDG_CONFIG_HOME/opencode/tui.json`.
|
|
|
|
This includes theme, keybinds, scroll settings, and other TUI-only options.
|
|
See <https://opencode.ai/docs/tui#configure> for the documentation.
|
|
|
|
Note that `"$schema": "https://opencode.ai/tui.json"` is automatically added.
|
|
|
|
Since OpenCode v1.2.15, TUI settings must be in a separate tui.json file.
|
|
Settings like `theme`, `keybinds`, and `tui` in {option}`programs.opencode.settings`
|
|
are deprecated and should be moved here.
|
|
|
|
OpenCode 2 no longer reads {file}`tui.json` and uses a single global
|
|
{file}`cli.json` instead. Use {option}`programs.opencode.cli` with
|
|
OpenCode 2; this option remains for OpenCode 1.
|
|
'';
|
|
};
|
|
|
|
cli = mkOption {
|
|
inherit (jsonFormat) type;
|
|
default = { };
|
|
example = {
|
|
theme = {
|
|
name = "system";
|
|
mode = "system";
|
|
};
|
|
tabs.mode = "auto";
|
|
};
|
|
|
|
description = ''
|
|
CLI-specific configuration written to
|
|
{file}`$XDG_CONFIG_HOME/opencode/cli.json`.
|
|
|
|
This includes theme, tabs, diffs, sessions, keybinds, and other
|
|
terminal-client settings. See
|
|
<https://opencode.ai/v2/docs/cli/config> for the documentation.
|
|
|
|
Note that `"$schema": "https://opencode.ai/v2/cli.json"` is automatically
|
|
added.
|
|
|
|
OpenCode 2 reads this file and rewrites it when settings are changed
|
|
from the TUI. If OpenCode has already created {file}`cli.json`, back up
|
|
or move the app-owned file before enabling this option, or enable
|
|
{option}`programs.opencode.mutableSettings` to keep declarative
|
|
settings in a separate layer.
|
|
|
|
OpenCode 1 reads {file}`tui.json` instead; use
|
|
{option}`programs.opencode.tui` for OpenCode 1.
|
|
'';
|
|
};
|
|
|
|
web = {
|
|
enable = lib.mkEnableOption "opencode web service";
|
|
|
|
extraArgs = mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ ];
|
|
example = [
|
|
"--hostname"
|
|
"0.0.0.0"
|
|
"--port"
|
|
"4096"
|
|
"--mdns"
|
|
"--cors"
|
|
"https://example.com"
|
|
"--cors"
|
|
"http://localhost:3000"
|
|
"--print-logs"
|
|
"--log-level"
|
|
"DEBUG"
|
|
];
|
|
description = ''
|
|
Extra arguments to pass to the opencode serve command.
|
|
|
|
These arguments override the "server" options defined in the configuration file.
|
|
See <https://opencode.ai/docs/web/#config-file> for available options.
|
|
'';
|
|
};
|
|
|
|
environmentFile = mkOption {
|
|
type = lib.types.nullOr lib.types.path;
|
|
default = null;
|
|
example = "/run/secrets/opencode-web";
|
|
description = ''
|
|
Path to a file containing environment variables for the opencode web
|
|
service, in the format of an EnvironmentFile as described by
|
|
{manpage}`systemd.exec(5)` (i.e. `KEY=VALUE` pairs, one per line).
|
|
|
|
This is the recommended way to set `OPENCODE_SERVER_PASSWORD` without
|
|
exposing the secret value in the Nix store.
|
|
'';
|
|
};
|
|
};
|
|
|
|
context = lib.mkOption {
|
|
type = lib.types.either lib.types.lines lib.types.path;
|
|
default = "";
|
|
description = ''
|
|
Global context for OpenCode.
|
|
|
|
The value is either:
|
|
- Inline content as a string
|
|
- A path to a file containing the content
|
|
|
|
The configured content is written to
|
|
{file}`$XDG_CONFIG_HOME/opencode/AGENTS.md`.
|
|
'';
|
|
example = lib.literalExpression ''
|
|
'''
|
|
# TypeScript Project Rules
|
|
|
|
## External File Loading
|
|
|
|
CRITICAL: When you encounter a file reference (e.g., @rules/general.md), use your Read tool to load it on a need-to-know basis. They're relevant to the SPECIFIC task at hand.
|
|
|
|
Instructions:
|
|
|
|
- Do NOT preemptively load all references - use lazy loading based on actual need
|
|
- When loaded, treat content as mandatory instructions that override defaults
|
|
- Follow references recursively when needed
|
|
|
|
## Development Guidelines
|
|
|
|
For TypeScript code style and best practices: @docs/typescript-guidelines.md
|
|
For React component architecture and hooks patterns: @docs/react-patterns.md
|
|
For REST API design and error handling: @docs/api-standards.md
|
|
For testing strategies and coverage requirements: @test/testing-guidelines.md
|
|
|
|
## General Guidelines
|
|
|
|
Read the following file immediately as it's relevant to all workflows: @rules/general-guidelines.md.
|
|
'''
|
|
'';
|
|
};
|
|
|
|
commands = lib.mkOption {
|
|
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.
|
|
|
|
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/commands/<name>.md`)
|
|
- A path to a file (creates `opencode/commands/<name>.md`)
|
|
|
|
If a path is used, it is expected to contain command files.
|
|
The directory is symlinked to {file}`$XDG_CONFIG_HOME/opencode/commands/`.
|
|
'';
|
|
example = lib.literalExpression ''
|
|
{
|
|
changelog = '''
|
|
# Update Changelog Command
|
|
|
|
Update CHANGELOG.md with a new entry for the specified version.
|
|
Usage: /changelog [version] [change-type] [message]
|
|
''';
|
|
fix-issue = ./commands/fix-issue.md;
|
|
commit = '''
|
|
# Commit Command
|
|
|
|
Create a git commit with proper message formatting.
|
|
Usage: /commit [message]
|
|
''';
|
|
}
|
|
'';
|
|
};
|
|
|
|
agents = lib.mkOption {
|
|
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.
|
|
|
|
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/agents/<name>.md`)
|
|
- A path to a file (creates `opencode/agents/<name>.md`)
|
|
|
|
If a path is used, it is expected to contain agent files.
|
|
The directory is symlinked to {file}`$XDG_CONFIG_HOME/opencode/agents/`.
|
|
'';
|
|
example = lib.literalExpression ''
|
|
{
|
|
code-reviewer = '''
|
|
# Code Reviewer Agent
|
|
|
|
You are a senior software engineer specializing in code reviews.
|
|
Focus on code quality, security, and maintainability.
|
|
|
|
## Guidelines
|
|
- Review for potential bugs and edge cases
|
|
- Check for security vulnerabilities
|
|
- Ensure code follows best practices
|
|
- Suggest improvements for readability and performance
|
|
''';
|
|
documentation = ./agents/documentation.md;
|
|
}
|
|
'';
|
|
};
|
|
|
|
skills = lib.mkOption {
|
|
type = lib.types.either (lib.types.attrsOf (
|
|
lib.types.oneOf [
|
|
lib.types.lines
|
|
lib.types.path
|
|
lib.types.str
|
|
]
|
|
)) lib.types.path;
|
|
default = { };
|
|
description = ''
|
|
Custom skills for OpenCode.
|
|
|
|
This option can be either:
|
|
- An attribute set defining skills
|
|
- A path to a directory containing skill folders
|
|
|
|
If an attribute set is used, the attribute name becomes the
|
|
skill directory name, and the value is either:
|
|
- Inline content as a string (creates `opencode/skills/<name>/SKILL.md`)
|
|
- A path to a file (creates `opencode/skills/<name>/SKILL.md`)
|
|
- A path to a directory (creates `opencode/skills/<name>/` with all files)
|
|
|
|
This also accepts Nix store paths, for example a skill directory
|
|
from a package.
|
|
|
|
If a path is used, it is expected to contain one folder per
|
|
skill name, each containing a {file}`SKILL.md`. The directory is
|
|
symlinked to {file}`$XDG_CONFIG_HOME/opencode/skills/`.
|
|
|
|
See <https://opencode.ai/docs/skills/> for the documentation.
|
|
'';
|
|
example = lib.literalExpression ''
|
|
{
|
|
git-release = '''
|
|
---
|
|
name: git-release
|
|
description: Create consistent releases and changelogs
|
|
---
|
|
|
|
## What I do
|
|
|
|
- Draft release notes from merged PRs
|
|
- Propose a version bump
|
|
- Provide a copy-pasteable `gh release create` command
|
|
''';
|
|
|
|
# A skill can also be a directory containing SKILL.md and other files.
|
|
data-analysis = ./skills/data-analysis;
|
|
|
|
# A skill can also be a subdirectory within a package source (store path)
|
|
beads = "''${pkgs.beads.src}/claude-plugin/skills/beads";
|
|
}
|
|
'';
|
|
};
|
|
|
|
themes = mkOption {
|
|
type = lib.types.either (lib.types.attrsOf (lib.types.either jsonFormat.type lib.types.path)) lib.types.path;
|
|
default = { };
|
|
description = ''
|
|
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.tui.theme` to enable the custom theme.
|
|
See <https://opencode.ai/docs/themes/> for the documentation.
|
|
'';
|
|
};
|
|
|
|
tools = lib.mkOption {
|
|
type = lib.types.either (lib.types.attrsOf (lib.types.either lib.types.lines lib.types.path)) lib.types.path;
|
|
default = { };
|
|
description = ''
|
|
Custom tools for opencode.
|
|
|
|
This option can either be:
|
|
- An attribute set defining tools
|
|
- A path to a directory containing multiple tool files
|
|
|
|
If an attribute set is used, the attribute name becomes the tool filename,
|
|
and the value is either:
|
|
- Inline content as a string (creates `opencode/tools/<name>.ts`)
|
|
- A path to a file (creates `opencode/tools/<name>.ts` or `opencode/tools/<name>.js`)
|
|
|
|
If a path is used, it is expected to contain tool files.
|
|
The directory is symlinked to {file}`$XDG_CONFIG_HOME/opencode/tools/`.
|
|
|
|
See <https://opencode.ai/docs/tools/> for the documentation.
|
|
'';
|
|
example = lib.literalExpression ''
|
|
{
|
|
database-query = '''
|
|
import { tool } from "@opencode-ai/plugin"
|
|
|
|
export default tool({
|
|
description: "Query the project database",
|
|
args: {
|
|
query: tool.schema.string().describe("SQL query to execute"),
|
|
},
|
|
async execute(args) {
|
|
// Your database logic here
|
|
return `Executed query: ''${args.query}`
|
|
},
|
|
})
|
|
''';
|
|
|
|
# Or reference an existing file
|
|
api-client = ./tools/api-client.ts;
|
|
}
|
|
'';
|
|
};
|
|
|
|
validateFiles = lib.mkOption {
|
|
type = lib.types.submodule {
|
|
options = {
|
|
config = lib.mkEnableOption ''
|
|
the validation of the generated configuration file
|
|
({file}`$XDG_CONFIG_HOME/opencode/opencode.json`, or
|
|
{file}`home-manager.json` when mutable configuration is enabled) against the
|
|
corresponding OpenCode JSON schema of the configured package using
|
|
{command}`check-jsonschema`.
|
|
'';
|
|
tui = lib.mkEnableOption ''
|
|
the validation of the generated configuration file
|
|
({file}`$XDG_CONFIG_HOME/opencode/tui.json`, or
|
|
{file}`home-manager-tui.json` when mutable configuration is enabled) against the
|
|
corresponding OpenCode JSON schema of the configured package using
|
|
{command}`check-jsonschema`. This requires an OpenCode 1 package;
|
|
OpenCode 2 no longer ships a {file}`tui.json` schema.
|
|
'';
|
|
cli = lib.mkEnableOption ''
|
|
the validation of the generated configuration file
|
|
({file}`$XDG_CONFIG_HOME/opencode/cli.json`, or
|
|
{file}`home-manager-cli.json` when mutable configuration is enabled) against the
|
|
corresponding OpenCode JSON schema of the configured package using
|
|
{command}`check-jsonschema`. This requires an OpenCode 2 package.
|
|
'';
|
|
};
|
|
};
|
|
default = { };
|
|
description = ''
|
|
Whether to validate the generated configuration files
|
|
({file}`$XDG_CONFIG_HOME/opencode/opencode.json`,
|
|
{file}`$XDG_CONFIG_HOME/opencode/cli.json`, and
|
|
{file}`$XDG_CONFIG_HOME/opencode/tui.json`) against the
|
|
corresponding OpenCode JSON schemas of the configured package
|
|
({option}`programs.opencode.package`, `config.json`, `cli.json`, and
|
|
`tui.json` from `passthru.jsonschema`) using
|
|
{command}`check-jsonschema`. When
|
|
{option}`programs.opencode.mutableSettings` is enabled, validation
|
|
applies to the generated {file}`home-manager.json`,
|
|
{file}`home-manager-cli.json`, and {file}`home-manager-tui.json` layers
|
|
instead of the writable user files.
|
|
'';
|
|
};
|
|
};
|
|
|
|
config = mkIf cfg.enable {
|
|
assertions = [
|
|
{
|
|
assertion =
|
|
(cfg.validateFiles.config || cfg.validateFiles.cli || cfg.validateFiles.tui) -> cfg.package != null;
|
|
message = "`programs.opencode.validateFiles` requires `programs.opencode.package` to be set";
|
|
}
|
|
{
|
|
assertion = !cfg.validateFiles.config || cfg.package == null || configSchema != null;
|
|
message = "`programs.opencode.validateFiles.config` requires `programs.opencode.package` to provide `passthru.jsonschema.config`";
|
|
}
|
|
{
|
|
assertion = !cfg.validateFiles.cli || cfg.package == null || cliSchema != null;
|
|
message = "`programs.opencode.validateFiles.cli` requires `programs.opencode.package` to provide `passthru.jsonschema.cli` (OpenCode 2 or later)";
|
|
}
|
|
{
|
|
assertion = !cfg.validateFiles.tui || cfg.package == null || tuiSchema != null;
|
|
message = "`programs.opencode.validateFiles.tui` requires `programs.opencode.package` to provide `passthru.jsonschema.tui` (OpenCode 1); OpenCode 2 moved TUI settings to `programs.opencode.cli`";
|
|
}
|
|
{
|
|
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.tools || lib.pathIsDirectory cfg.tools;
|
|
message = "`programs.opencode.tools` 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";
|
|
}
|
|
];
|
|
|
|
warnings =
|
|
let
|
|
deprecatedConfigKeys = lib.filter (
|
|
k:
|
|
lib.elem k [
|
|
"theme"
|
|
"keybinds"
|
|
"tui"
|
|
]
|
|
) (lib.attrNames cfg.settings);
|
|
|
|
showDeprecatedKeysWarning =
|
|
deprecatedConfigKeys != [ ]
|
|
&& (packageVersion == null || lib.versionAtLeast packageVersion "1.2.15");
|
|
terminalOption = if isOpenCode2 then "programs.opencode.cli" else "programs.opencode.tui";
|
|
terminalFile = if isOpenCode2 then "cli.json" else "tui.json";
|
|
terminalDocs =
|
|
if isOpenCode2 then
|
|
"https://opencode.ai/v2/docs/cli/config"
|
|
else
|
|
"https://opencode.ai/docs/config#tui";
|
|
in
|
|
lib.optionals showDeprecatedKeysWarning [
|
|
''
|
|
programs.opencode.settings contains deprecated TUI-specific keys: ${lib.concatStringsSep ", " deprecatedConfigKeys}
|
|
|
|
These settings should be moved to ${terminalOption} instead.
|
|
|
|
OpenCode requires terminal client settings in ${terminalFile}.
|
|
See: ${terminalDocs}
|
|
''
|
|
]
|
|
++ lib.optionals (cfg.tui != { } && isOpenCode2) [
|
|
''
|
|
programs.opencode.tui is ignored by OpenCode 2.
|
|
|
|
OpenCode 2 replaced tui.json with a single global cli.json. Move
|
|
terminal client settings to programs.opencode.cli. When
|
|
programs.opencode.mutableSettings is enabled, OPENCODE_TUI_CONFIG is
|
|
ignored as well.
|
|
|
|
OpenCode 2 only migrates an existing tui.json when cli.json does not
|
|
exist. See: https://opencode.ai/v2/docs/cli/config
|
|
''
|
|
]
|
|
++ lib.optionals (hasCli && !isOpenCode2) [
|
|
''
|
|
programs.opencode.cli requires OpenCode 2 or later, but the configured
|
|
package is version ${packageVersion}.
|
|
|
|
Use programs.opencode.tui for OpenCode 1, or update
|
|
programs.opencode.package.
|
|
''
|
|
];
|
|
|
|
home = {
|
|
packages = mkIf (packageWithExtraPackages != null) [ packageWithExtraPackages ];
|
|
|
|
sessionVariables = lib.mapAttrs (
|
|
_: value: lib.escape [ "\\" "\"" "$" "`" ] value
|
|
) mutableEnvironment;
|
|
};
|
|
|
|
xdg.configFile = {
|
|
"${settingsPath}" = mkIf hasSettings {
|
|
source =
|
|
let
|
|
# Merge MCP servers: transformed servers + user settings, with user settings taking precedence
|
|
mergedMcpServers = transformedMcpServers // (cfg.settings.mcp or { });
|
|
# Merge all settings
|
|
mergedSettings =
|
|
cfg.settings // (lib.optionalAttrs (mergedMcpServers != { }) { mcp = mergedMcpServers; });
|
|
orderedJsonFormat = lib.hm.generators.mkDAGOrderedJsonFormat {
|
|
inherit pkgs jsonFormat;
|
|
schema = if cfg.validateFiles.config then configSchema else null;
|
|
};
|
|
in
|
|
orderedJsonFormat.generate "opencode.json" (
|
|
{
|
|
"$schema" = "https://opencode.ai/config.json";
|
|
}
|
|
// mergedSettings
|
|
);
|
|
};
|
|
|
|
"${tuiPath}" = mkIf (cfg.tui != { }) (
|
|
let
|
|
orderedJsonFormat = lib.hm.generators.mkDAGOrderedJsonFormat {
|
|
inherit pkgs jsonFormat;
|
|
schema = if cfg.validateFiles.tui then tuiSchema else null;
|
|
};
|
|
in
|
|
{
|
|
source = orderedJsonFormat.generate "tui.json" (
|
|
{
|
|
"$schema" = "https://opencode.ai/tui.json";
|
|
}
|
|
// cfg.tui
|
|
);
|
|
}
|
|
);
|
|
|
|
"${cliPath}" = mkIf hasCli (
|
|
let
|
|
cliFormat = lib.hm.generators.mkDAGOrderedJsonFormat {
|
|
inherit pkgs jsonFormat;
|
|
schema = if cfg.validateFiles.cli then cliSchema else null;
|
|
};
|
|
in
|
|
{
|
|
source = cliFormat.generate "opencode-cli.json" cliConfig;
|
|
}
|
|
);
|
|
|
|
"opencode/AGENTS.md" = (
|
|
if lib.isPath cfg.context then
|
|
{ source = cfg.context; }
|
|
else
|
|
(mkIf (cfg.context != "") {
|
|
text = cfg.context;
|
|
})
|
|
);
|
|
|
|
"opencode/commands" = mkIf (lib.isPath cfg.commands) {
|
|
source = cfg.commands;
|
|
recursive = true;
|
|
};
|
|
|
|
"opencode/agents" = mkIf (lib.isPath cfg.agents) {
|
|
source = cfg.agents;
|
|
recursive = true;
|
|
};
|
|
|
|
"opencode/tools" = mkIf (lib.isPath cfg.tools) {
|
|
source = cfg.tools;
|
|
recursive = true;
|
|
};
|
|
|
|
"opencode/skills" = mkIf (lib.hm.strings.isPathLike cfg.skills) {
|
|
source = normalizeDirectory "opencode-skills" cfg.skills;
|
|
recursive = true;
|
|
};
|
|
|
|
"opencode/themes" = mkIf (lib.isPath cfg.themes) {
|
|
source = cfg.themes;
|
|
recursive = true;
|
|
};
|
|
}
|
|
// lib.optionalAttrs (builtins.isAttrs cfg.commands) (
|
|
lib.mapAttrs' (
|
|
name: content:
|
|
lib.nameValuePair "opencode/commands/${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/agents/${name}.md" (
|
|
if lib.isPath content then { source = content; } else { text = content; }
|
|
)
|
|
) cfg.agents
|
|
)
|
|
// lib.optionalAttrs (builtins.isAttrs cfg.tools) (
|
|
lib.mapAttrs' (
|
|
name: content:
|
|
lib.nameValuePair "opencode/tools/${name}.ts" (
|
|
if lib.isPath content then { source = content; } else { text = content; }
|
|
)
|
|
) cfg.tools
|
|
)
|
|
// lib.mapAttrs' (
|
|
name: content:
|
|
if lib.isPath content && lib.pathIsDirectory content then
|
|
lib.nameValuePair "opencode/skills/${name}" {
|
|
source = content;
|
|
recursive = true;
|
|
}
|
|
else if lib.hm.strings.isPathLike content && !lib.isPath content then
|
|
lib.nameValuePair "opencode/skills/${name}" {
|
|
source = normalizeSkill content;
|
|
recursive = true;
|
|
}
|
|
else
|
|
lib.nameValuePair "opencode/skills/${name}/SKILL.md" (
|
|
if lib.hm.strings.isPathLike content then { source = content; } else { text = content; }
|
|
)
|
|
) (if builtins.isAttrs cfg.skills then cfg.skills else { })
|
|
// 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
|
|
);
|
|
|
|
systemd.user.services = mkIf webCfg.enable {
|
|
opencode-web = {
|
|
Unit = {
|
|
Description = "OpenCode Web Service";
|
|
After = [ "network.target" ];
|
|
};
|
|
|
|
Service = {
|
|
ExecStart = lib.getExe opencodeWebLauncher;
|
|
Restart = "always";
|
|
RestartSec = 5;
|
|
}
|
|
// lib.optionalAttrs (webCfg.environmentFile != null) {
|
|
EnvironmentFile = webCfg.environmentFile;
|
|
};
|
|
|
|
Install = {
|
|
WantedBy = [ "default.target" ];
|
|
};
|
|
};
|
|
};
|
|
|
|
launchd.agents = mkIf webCfg.enable {
|
|
opencode-web = {
|
|
enable = true;
|
|
config = {
|
|
ProgramArguments =
|
|
let
|
|
opencodeLaunchdWrapper = pkgs.writeShellScriptBin "opencode-launchd-wrapper" ''
|
|
source ${webCfg.environmentFile}
|
|
exec ${lib.getExe opencodeWebLauncher}
|
|
'';
|
|
in
|
|
if webCfg.environmentFile == null then
|
|
[ (lib.getExe opencodeWebLauncher) ]
|
|
else
|
|
[
|
|
(lib.getExe opencodeLaunchdWrapper)
|
|
];
|
|
KeepAlive = {
|
|
Crashed = true;
|
|
SuccessfulExit = false;
|
|
};
|
|
ProcessType = "Background";
|
|
RunAtLoad = true;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
}
|