diff --git a/modules/misc/news/2026/04/2026-04-17_17-06-14.nix b/modules/misc/news/2026/04/2026-04-17_17-06-14.nix new file mode 100644 index 000000000..e19ab725e --- /dev/null +++ b/modules/misc/news/2026/04/2026-04-17_17-06-14.nix @@ -0,0 +1,15 @@ +_: { + time = "2026-04-17T15:06:14+00:00"; + condition = true; + message = '' + A new module is available: 'programs.github-copilot-cli'. + + GitHub Copilot CLI brings the agentic Copilot coding experience to the + terminal. The module manages the `~/.copilot/config.json` settings file + (model, theme, trusted folders, hooks, feature flags, etc.) and the + `~/.copilot/mcp-config.json` MCP server registry. Setting + `enableMcpIntegration = true` reuses servers defined under + `programs.mcp.servers`, with `programs.github-copilot-cli.mcpServers` + taking precedence. + ''; +} diff --git a/modules/programs/github-copilot-cli.nix b/modules/programs/github-copilot-cli.nix new file mode 100644 index 000000000..c9971c1a0 --- /dev/null +++ b/modules/programs/github-copilot-cli.nix @@ -0,0 +1,200 @@ +{ + lib, + pkgs, + config, + ... +}: +let + inherit (lib) + literalExpression + mkEnableOption + mkIf + mkOption + mkPackageOption + ; + + cfg = config.programs.github-copilot-cli; + + jsonFormat = pkgs.formats.json { }; + + upstreamConfigDir = "${config.home.homeDirectory}/.copilot"; + + transformSingleServer = + _name: server: + let + base = removeAttrs server [ "disabled" ]; + withType = + if base ? type then + base + else if base ? url then + base // { type = "http"; } + else + base // { type = "local"; }; + withTools = if withType ? tools then withType else withType // { tools = [ "*" ]; }; + in + withTools; + + transformedMcpServers = + if cfg.enableMcpIntegration && config.programs.mcp.enable && config.programs.mcp.servers != { } then + lib.mapAttrs transformSingleServer ( + lib.filterAttrs ( + _: server: !(server.disabled or false) && (server ? url || server ? command) + ) config.programs.mcp.servers + ) + else + { }; + + mergedMcpServers = transformedMcpServers // cfg.mcpServers; +in +{ + meta.maintainers = [ lib.maintainers.ojsef39 ]; + + options.programs.github-copilot-cli = { + enable = mkEnableOption "GitHub Copilot CLI"; + + package = mkPackageOption pkgs "github-copilot-cli" { nullable = true; }; + + configDir = mkOption { + type = lib.types.str; + default = + if config.home.preferXdgDirectories then "${config.xdg.configHome}/copilot" else upstreamConfigDir; + defaultText = literalExpression '' + if config.home.preferXdgDirectories then + "''${config.xdg.configHome}/copilot" + else + "''${config.home.homeDirectory}/.copilot" + ''; + example = literalExpression ''"''${config.xdg.configHome}/copilot"''; + description = '' + Directory holding Copilot CLI configuration files such as + {file}`config.json` and {file}`mcp-config.json`. + + Defaults to `''${config.xdg.configHome}/copilot` when + {option}`home.preferXdgDirectories` is enabled and to `~/.copilot` + otherwise. The {env}`COPILOT_HOME` environment variable is exported + automatically whenever the directory differs from the upstream + default of `~/.copilot`. + + See . + ''; + }; + + enableMcpIntegration = mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether to integrate the MCP servers config from + {option}`programs.mcp.servers` into + {option}`programs.github-copilot-cli.mcpServers`. + + Servers defined in {option}`programs.mcp.servers` are merged with + {option}`programs.github-copilot-cli.mcpServers`, with the latter + taking precedence. Disabled servers (where `disabled = true`) are + excluded from the generated configuration. + ''; + }; + + settings = mkOption { + type = lib.types.attrsOf jsonFormat.type; + default = { }; + example = literalExpression '' + { + model = "claude-sonnet-4-5"; + theme = "default"; + trusted_folders = [ "/home/user/projects" ]; + renderMarkdown = true; + autoUpdate = false; + } + ''; + description = '' + Configuration written to {file}`config.json` inside + {option}`programs.github-copilot-cli.configDir`. + + Known configuration keys include: + - `model` — AI model selection + - `effortLevel` — reasoning effort for capable models + - `theme` — `"default"`, `"dim"`, `"high-contrast"`, or `"colorblind"` + - `mouse` — enable mouse support (default: `true`) + - `banner` — frequency of animated banner display + - `renderMarkdown` — markdown rendering toggle (default: `true`) + - `screenReader` — accessibility optimizations (default: `false`) + - `autoUpdate` — automatic CLI updates (default: `true`) + - `stream` — token-by-token response streaming (default: `true`) + - `includeCoAuthoredBy` — agent commit attribution (default: `true`) + - `respectGitignore` — exclude gitignored files from file picker + - `trusted_folders` — list of pre-approved directory paths + - `allowed_urls`, `denied_urls` — URL allowlists/blocklists + - `logLevel` — log verbosity + - `disableAllHooks` — global hook disable toggle + - `hooks` — inline hook definitions + - `enabledFeatureFlags` — enable or disable specific feature flags + + See + for the documentation. + ''; + }; + + mcpServers = mkOption { + type = lib.types.attrsOf jsonFormat.type; + default = { }; + example = literalExpression '' + { + playwright = { + type = "local"; + command = "npx"; + args = [ "@playwright/mcp@latest" ]; + tools = [ "*" ]; + }; + context7 = { + type = "http"; + url = "https://mcp.context7.com/mcp"; + headers = { CONTEXT7_API_KEY = "YOUR-API-KEY"; }; + tools = [ "*" ]; + }; + } + ''; + description = '' + MCP server configurations written to {file}`mcp-config.json` inside + {option}`programs.github-copilot-cli.configDir`. + + Each attribute defines a server entry under `mcpServers` in the config + file. Supported server types: + - `local` — starts a local process via stdio (`command`, optional `args`, `env`) + - `http` — connects to a remote HTTP server (`url`, optional `headers`) + - `sse` — legacy HTTP with Server-Sent Events (same structure as `http`) + + The `tools` field accepts `["*"]` to enable all tools or a list of + specific tool names. + + See + for the documentation. + ''; + }; + }; + + config = mkIf cfg.enable { + home.packages = mkIf (cfg.package != null) [ cfg.package ]; + + home.file = { + # NOTE: Copilot will try to add a firstLaunchAt date and crash if the + # file exists but does not have this key set. Only generate the file when + # the user has explicitly configured settings, and always inject the + # default so the managed file stays valid. + "${cfg.configDir}/config.json" = mkIf (cfg.settings != { }) { + source = jsonFormat.generate "github-copilot-cli-config.json" ( + { firstLaunchAt = "1970-01-01T00:00:00.000Z"; } // cfg.settings + ); + }; + + "${cfg.configDir}/mcp-config.json" = mkIf (mergedMcpServers != { }) { + source = jsonFormat.generate "github-copilot-cli-mcp-config.json" { + mcpServers = mergedMcpServers; + }; + }; + }; + + home.sessionVariables = mkIf (cfg.configDir != upstreamConfigDir) { + COPILOT_HOME = cfg.configDir; + }; + }; +} diff --git a/tests/darwinScrublist.nix b/tests/darwinScrublist.nix index cd1e610c0..183b5b67f 100644 --- a/tests/darwinScrublist.nix +++ b/tests/darwinScrublist.nix @@ -68,6 +68,7 @@ let "git-credential-oauth" "git-lfs" "git-worktree-switcher" + "github-copilot-cli" "gitMinimal" "gnupg" "go" diff --git a/tests/modules/programs/github-copilot-cli/config.nix b/tests/modules/programs/github-copilot-cli/config.nix new file mode 100644 index 000000000..8d725e0d1 --- /dev/null +++ b/tests/modules/programs/github-copilot-cli/config.nix @@ -0,0 +1,17 @@ +{ + programs.github-copilot-cli = { + enable = true; + settings = { + model = "claude-sonnet-4-5"; + theme = "dark"; + trusted_folders = [ "/home/user/projects" ]; + }; + }; + + nmt.script = '' + assertFileExists home-files/.copilot/config.json + assertFileContent home-files/.copilot/config.json ${./expected-config.json} + assertPathNotExists home-files/.copilot/mcp-config.json + assertFileNotRegex home-path/etc/profile.d/hm-session-vars.sh 'COPILOT_HOME' + ''; +} diff --git a/tests/modules/programs/github-copilot-cli/default.nix b/tests/modules/programs/github-copilot-cli/default.nix new file mode 100644 index 000000000..64349b224 --- /dev/null +++ b/tests/modules/programs/github-copilot-cli/default.nix @@ -0,0 +1,6 @@ +{ + github-copilot-cli-config = ./config.nix; + github-copilot-cli-mcp = ./mcp.nix; + github-copilot-cli-mcp-integration = ./mcp-integration.nix; + github-copilot-cli-xdg-config-dir = ./xdg-config-dir.nix; +} diff --git a/tests/modules/programs/github-copilot-cli/expected-config.json b/tests/modules/programs/github-copilot-cli/expected-config.json new file mode 100644 index 000000000..d2f594c2f --- /dev/null +++ b/tests/modules/programs/github-copilot-cli/expected-config.json @@ -0,0 +1,8 @@ +{ + "firstLaunchAt": "1970-01-01T00:00:00.000Z", + "model": "claude-sonnet-4-5", + "theme": "dark", + "trusted_folders": [ + "/home/user/projects" + ] +} diff --git a/tests/modules/programs/github-copilot-cli/expected-mcp-config.json b/tests/modules/programs/github-copilot-cli/expected-mcp-config.json new file mode 100644 index 000000000..1532d5401 --- /dev/null +++ b/tests/modules/programs/github-copilot-cli/expected-mcp-config.json @@ -0,0 +1,21 @@ +{ + "mcpServers": { + "context7": { + "tools": [ + "*" + ], + "type": "http", + "url": "https://mcp.context7.com/mcp" + }, + "playwright": { + "args": [ + "@playwright/mcp@latest" + ], + "command": "npx", + "tools": [ + "*" + ], + "type": "local" + } + } +} diff --git a/tests/modules/programs/github-copilot-cli/expected-mcp-integration-config.json b/tests/modules/programs/github-copilot-cli/expected-mcp-integration-config.json new file mode 100644 index 000000000..58ca793e9 --- /dev/null +++ b/tests/modules/programs/github-copilot-cli/expected-mcp-integration-config.json @@ -0,0 +1,30 @@ +{ + "mcpServers": { + "database": { + "args": [ + "-y", + "@bytebase/dbhub" + ], + "command": "npx", + "env": { + "DATABASE_URL": "postgresql://user:pass@localhost:5432/db" + }, + "tools": [ + "*" + ], + "type": "local" + }, + "filesystem": { + "args": [ + "-y", + "@modelcontextprotocol/server-filesystem", + "/tmp" + ], + "command": "npx", + "tools": [ + "*" + ], + "type": "local" + } + } +} diff --git a/tests/modules/programs/github-copilot-cli/mcp-integration.nix b/tests/modules/programs/github-copilot-cli/mcp-integration.nix new file mode 100644 index 000000000..a60fbee99 --- /dev/null +++ b/tests/modules/programs/github-copilot-cli/mcp-integration.nix @@ -0,0 +1,51 @@ +{ + programs = { + github-copilot-cli = { + enable = true; + enableMcpIntegration = true; + # user-defined server takes precedence over the integrated one + mcpServers.filesystem = { + type = "local"; + command = "npx"; + args = [ + "-y" + "@modelcontextprotocol/server-filesystem" + "/tmp" + ]; + tools = [ "*" ]; + }; + }; + mcp = { + enable = true; + servers = { + filesystem = { + command = "npx"; + args = [ + "-y" + "@modelcontextprotocol/server-filesystem" + "/other-tmp" + ]; + }; + database = { + command = "npx"; + args = [ + "-y" + "@bytebase/dbhub" + ]; + env = { + DATABASE_URL = "postgresql://user:pass@localhost:5432/db"; + }; + }; + disabled-server = { + command = "disabled-cmd"; + disabled = true; + }; + }; + }; + }; + + nmt.script = '' + assertFileExists home-files/.copilot/mcp-config.json + assertFileContent home-files/.copilot/mcp-config.json ${./expected-mcp-integration-config.json} + ''; +} diff --git a/tests/modules/programs/github-copilot-cli/mcp.nix b/tests/modules/programs/github-copilot-cli/mcp.nix new file mode 100644 index 000000000..7bd4c558f --- /dev/null +++ b/tests/modules/programs/github-copilot-cli/mcp.nix @@ -0,0 +1,24 @@ +{ + programs.github-copilot-cli = { + enable = true; + mcpServers = { + playwright = { + type = "local"; + command = "npx"; + args = [ "@playwright/mcp@latest" ]; + tools = [ "*" ]; + }; + context7 = { + type = "http"; + url = "https://mcp.context7.com/mcp"; + tools = [ "*" ]; + }; + }; + }; + + nmt.script = '' + assertFileExists home-files/.copilot/mcp-config.json + assertFileContent home-files/.copilot/mcp-config.json ${./expected-mcp-config.json} + assertPathNotExists home-files/.copilot/config.json + ''; +} diff --git a/tests/modules/programs/github-copilot-cli/xdg-config-dir.nix b/tests/modules/programs/github-copilot-cli/xdg-config-dir.nix new file mode 100644 index 000000000..b7c1ddcfd --- /dev/null +++ b/tests/modules/programs/github-copilot-cli/xdg-config-dir.nix @@ -0,0 +1,19 @@ +{ + home.preferXdgDirectories = true; + programs.github-copilot-cli = { + enable = true; + settings = { + model = "claude-sonnet-4-5"; + theme = "dark"; + trusted_folders = [ "/home/user/projects" ]; + }; + }; + + nmt.script = '' + assertFileExists home-files/.config/copilot/config.json + assertFileContent home-files/.config/copilot/config.json ${./expected-config.json} + assertPathNotExists home-files/.copilot/config.json + assertFileContains home-path/etc/profile.d/hm-session-vars.sh \ + 'export COPILOT_HOME="/home/hm-user/.config/copilot"' + ''; +}