Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
621d9b169b | ||
|
|
4ce190229c | ||
|
|
3cd22efe64 | ||
|
|
af2beae5f0 | ||
|
|
868d0a692d | ||
|
|
7bfff44b46 | ||
|
|
78671d3ce3 | ||
|
|
ad026da57b | ||
|
|
8355f0a16b | ||
|
|
ea6d221d7a | ||
|
|
005c72cc1a | ||
|
|
22d5ec3d12 | ||
|
|
d899b01766 | ||
|
|
d136d10d61 | ||
|
|
55cf7c9428 | ||
|
|
c88f017a22 | ||
|
|
d48a1e6ad6 | ||
|
|
8982f45135 | ||
|
|
ba6a244f3b | ||
|
|
cbc74b9371 | ||
|
|
fcbbf5484a | ||
|
|
baa46aeb6d | ||
|
|
4eb4fec416 | ||
|
|
e28654b710 | ||
|
|
4baa8ac595 | ||
|
|
b179bde238 | ||
|
|
0a56fc5a57 |
+1
-1
@@ -10,7 +10,7 @@
|
||||
- modules/programs/claude-code.nix
|
||||
- modules/programs/codex.nix
|
||||
- modules/programs/fabric-ai.nix
|
||||
- modules/programs/gemini-cli.nix
|
||||
- modules/programs/antigravity-cli.nix
|
||||
- modules/programs/mcp.nix
|
||||
- modules/programs/mistral-vibe.nix
|
||||
- modules/programs/mods.nix
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
with:
|
||||
app-id: ${{ vars.CI_APP_ID }}
|
||||
private-key: ${{ secrets.CI_APP_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }}
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
os: [ubuntu-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: cachix/install-nix-action@v31
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
echo "Found $COUNT changed module files."
|
||||
echo "count=$COUNT" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: actions/labeler@v6
|
||||
- uses: actions/labeler@v7
|
||||
if: steps.count.outputs.count <= 10
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
app-id: ${{ vars.CI_APP_ID }}
|
||||
private-key: ${{ secrets.CI_APP_PRIVATE_KEY }}
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
- name: Get Nixpkgs revision from flake.lock
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
parse: ${{ steps.changes.outputs.parse }}
|
||||
tests: ${{ steps.changes.outputs.tests }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dorny/paths-filter@v4
|
||||
id: changes
|
||||
with:
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: Get Nixpkgs revision from flake.lock
|
||||
id: get-nixpkgs
|
||||
run: |
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
echo "email=$id+$name@users.noreply.github.com"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token || github.token }}
|
||||
- name: Get Nixpkgs revision from flake.lock
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
if: github.repository_owner == 'nix-community'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
- name: Get Nixpkgs revision from flake.lock
|
||||
id: get-nixpkgs
|
||||
run: |
|
||||
|
||||
Generated
+4
-4
@@ -2,16 +2,16 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1773821835,
|
||||
"narHash": "sha256-wvfdLLWJ2I9oEpDd9PfMA8osfIZicoQ5MT1jIwNs9Tk=",
|
||||
"lastModified": 1779622335,
|
||||
"narHash": "sha256-ViA62qtL5za7V3d5I8OA9q9JcFhsVAiL5jVHwEclWqk=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "c06b4ae3d6599a672a6210b7021d699c351eebda",
|
||||
"rev": "705e9929918b43bd7b715dc0a878ac870449bb03",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"ref": "nixos-26.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
description = "Support developing Home Manager documentation";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
|
||||
scss-reset = {
|
||||
url = "github:andreymatin/scss-reset/1.4.2";
|
||||
flake = false;
|
||||
|
||||
+154
-36
@@ -6,41 +6,141 @@ The 26.05 release branch became stable in May, 2026.
|
||||
|
||||
This release has the following notable changes:
|
||||
|
||||
- WezTerm now supports declarative configuration via
|
||||
[](#opt-programs.wezterm.settings). Settings are expressed as a Nix
|
||||
attribute set and serialized to Lua using `lib.generators.toLua`.
|
||||
Raw Lua expressions such as `wezterm.font` and `wezterm.action.*`
|
||||
can be embedded using `lib.generators.mkLuaInline`. The existing
|
||||
[](#opt-programs.wezterm.extraConfig) option remains fully supported
|
||||
and can be combined with [](#opt-programs.wezterm.settings).
|
||||
- Home Manager now supports RFC 42-style SSH configuration through
|
||||
[](#opt-programs.ssh.settings). The legacy `programs.ssh.matchBlocks` format
|
||||
is deprecated and migrated automatically.
|
||||
|
||||
- The [](#opt-programs.anki.uiScale) option now expects a value in the
|
||||
range 1.0–2.0, previously it erroneously expected values in the
|
||||
range `0.0–1.0`.
|
||||
- The new `home-manager.startAsUserService` option runs user activation on
|
||||
demand at login instead of doing all setup during system boot, which helps on
|
||||
systems where home directories are mounted later (for example, pam_mount).
|
||||
|
||||
- New options were added for `services.podman.useDefaultMachine` and podman
|
||||
machine management on Darwin.
|
||||
|
||||
- The [](#opt-programs.rclone.enable) module now supports launchd-backed config,
|
||||
mount, and serve agents on Darwin. The new
|
||||
`programs.rclone.remotes.<name>.serve` option can manage `rclone serve`
|
||||
sidecar services on both Linux and Darwin. On non-systemd platforms,
|
||||
`programs.rclone.requiresUnit` is read-only and always `null` because there is
|
||||
no systemd ordering unit to depend on.
|
||||
|
||||
- The Syncthing credentials flow now uses `services.syncthing.guiCredentials`
|
||||
instead of the removed `services.syncthing.passwordFile` option.
|
||||
|
||||
- Firefox extensions are now managed per-profile through the
|
||||
`programs.firefox.profiles.<name>.extensions.packages` and
|
||||
`programs.firefox.profiles.<name>.extensions.settings` options. The top-level
|
||||
`programs.firefox.extensions = [ ... ]` list was removed; migrate it to
|
||||
`programs.firefox.profiles.<name>.extensions.packages`.
|
||||
|
||||
- Thunderbird now supports `accounts.email.accounts.<name>.ews`, the
|
||||
`outlook.office365.com-ews` account flavor, declarative language packs, and
|
||||
enterprise policies. Thunderbird accounts using the `outlook.office365.com`
|
||||
flavor now default IMAP and SMTP authentication to OAuth2 when the account
|
||||
does not set an authentication method explicitly.
|
||||
|
||||
- A new `systemd.user.packages` option provides a user-level equivalent of
|
||||
`systemd.packages` for installing unit files from packages.
|
||||
|
||||
- WezTerm now supports declarative configuration via
|
||||
[](#opt-programs.wezterm.settings). Settings are expressed as a Nix attribute
|
||||
set and serialized to Lua using `lib.generators.toLua`. Raw Lua expressions
|
||||
such as `wezterm.font` and `wezterm.action.*` can be embedded using
|
||||
`lib.generators.mkLuaInline`. The existing
|
||||
[](#opt-programs.wezterm.extraConfig) option remains fully supported and can
|
||||
be combined with [](#opt-programs.wezterm.settings).
|
||||
|
||||
- The [](#opt-programs.anki.uiScale) option now expects a value in the range
|
||||
1.0–2.0, previously it erroneously expected values in the range `0.0–1.0`.
|
||||
Anki sync options moved from `programs.anki.sync.*` to
|
||||
`programs.anki.profiles."User 1".sync.*`, and
|
||||
`programs.anki.sync.passwordFile` is migrated to
|
||||
`programs.anki.profiles."User 1".sync.keyFile`.
|
||||
|
||||
- New [](#opt-home.services) namespace for nixpkgs
|
||||
[modular services](https://nixos.org/manual/nixos/unstable/#modular-services).
|
||||
Service modules shipped with packages (e.g.
|
||||
`pkgs.<name>.passthru.services.default`) drop in unchanged and are
|
||||
lifted to user systemd units. See
|
||||
[Modular Services](#sec-usage-modular-services) for details.
|
||||
`pkgs.<name>.passthru.services.default`) drop in unchanged and are lifted to
|
||||
user systemd units. See [Modular Services](#sec-usage-modular-services) for
|
||||
details.
|
||||
|
||||
- Dedicated modules were added for several VSCode forks (`programs.cursor`,
|
||||
`programs.vscodium`, `programs.windsurf`, `programs.kiro`, and
|
||||
`programs.antigravity`), built from a shared `mkVscodeModule` factory. Each
|
||||
fork can now be configured independently and simultaneously. Users who
|
||||
previously selected a fork by setting `programs.vscode.package` should migrate
|
||||
to the matching module; the `programs.vscode.pname` option was removed.
|
||||
|
||||
- The AI coding-assistant modules ([](#opt-programs.claude-code.enable),
|
||||
[](#opt-programs.codex.enable), and [](#opt-programs.opencode.enable)) now
|
||||
share a unified `context` option for providing global instructions. The
|
||||
previous per-module options (`claude-code.memory`,
|
||||
`codex.custom-instructions`, and `opencode.rules`) are deprecated and migrated
|
||||
automatically. MCP server configuration via `programs.mcp.servers` was also
|
||||
extended to more assistant and editor modules through an
|
||||
`enableMcpIntegration` option.
|
||||
|
||||
- The `programs.man` module was split so the man viewer can be selected through
|
||||
the [](#opt-programs.man.man-db.enable) and
|
||||
[](#opt-programs.man.mandoc.enable) submodules, which are mutually exclusive.
|
||||
|
||||
- The `programs.neovim.extraLuaConfig` option was renamed to
|
||||
[](#opt-programs.neovim.initLua). Home Manager now writes generated Lua
|
||||
initialization to {file}`$XDG_CONFIG_HOME/nvim/init.lua` by default. If you
|
||||
manage that file outside Home Manager, set
|
||||
[](#opt-programs.neovim.sideloadInitLua) to `true` to load the generated Lua
|
||||
through the Neovim wrapper instead.
|
||||
|
||||
- A new `sshAuthSock` module manages the `SSH_AUTH_SOCK`
|
||||
environment variable. It is implicitly configured by the
|
||||
SSH-agent-providing modules (e.g. `services.ssh-agent`, `services.gpg-agent`).
|
||||
The old `services.ssh-agent.enable{Bash,Zsh,Fish,Nushell}Integration` options
|
||||
were removed; shell initialization is now provided through `sshAuthSock`.
|
||||
|
||||
- A new [](#opt-services.pipewire.enable) module provides client-side
|
||||
configuration for PipeWire, WirePlumber, and the PulseAudio/JACK compatibility
|
||||
layers. It does not install or manage the PipeWire daemon itself.
|
||||
|
||||
- The `services.swww` module was renamed to [](#opt-services.awww.enable)
|
||||
following the upstream project rename. The `enable`, `package`, and
|
||||
`extraArgs` options are migrated automatically from `services.swww.*` to
|
||||
`services.awww.*`.
|
||||
|
||||
- Several modules dropped their freeform `extraConfig` escape hatches in favor
|
||||
of RFC 42-style `settings`: `programs.aerospace.extraConfig` (and
|
||||
`userSettings`) and `programs.aria2.extraConfig` were removed.
|
||||
`programs.mise.settings` was renamed to `programs.mise.globalConfig.settings`.
|
||||
The `programs.eww.configDir` and
|
||||
`programs.eww.enable{Bash,Zsh,Fish}Integration` options were removed; use
|
||||
[](#opt-programs.eww.yuckConfig) and [](#opt-programs.eww.scssConfig) for
|
||||
declarative Eww config files. The `programs.niriswitcher` options were also
|
||||
removed.
|
||||
|
||||
- Darwin integration received several behavior changes. Home Manager launchd
|
||||
agents now wait for `/nix/store` before starting and use
|
||||
`launchctl bootout --wait` when replacing agents. nix-darwin activations now
|
||||
propagate dry-run mode into Home Manager user activations. On Darwin, Home
|
||||
Manager also exports `TERMINFO_DIRS` so terminfo entries from Home
|
||||
Manager-installed packages are visible to shells.
|
||||
|
||||
- The [](#opt-nix.assumeXdg) option was added for installations where Nix is
|
||||
configured to use XDG base directories outside Home Manager. Home Manager also
|
||||
detects `osConfig.nix.settings.use-xdg-base-directories` when deciding where
|
||||
to place declarative channel entries.
|
||||
|
||||
## State Version Changes {#sec-release-26.05-state-version-changes}
|
||||
|
||||
The state version in this release includes the changes below. These
|
||||
changes are only active if the `home.stateVersion` option is set to
|
||||
\"26.05\" or later.
|
||||
The state version in this release includes the changes below. These changes are
|
||||
only active if the `home.stateVersion` option is set to \"26.05\" or later.
|
||||
|
||||
- The [](#opt-gtk.gtk4.theme) option does not mirror
|
||||
[](#opt-gtk.theme) by default anymore.
|
||||
- The [](#opt-gtk.gtk4.theme) option does not mirror [](#opt-gtk.theme) by
|
||||
default anymore.
|
||||
|
||||
- The [](#opt-programs.zsh.dotDir) option now defaults to the XDG
|
||||
configuration directory (usually `~/.config/zsh`) when
|
||||
[](#opt-xdg.enable) is true.
|
||||
- The [](#opt-programs.zsh.dotDir) option now defaults to the XDG configuration
|
||||
directory (usually `~/.config/zsh`) when [](#opt-xdg.enable) is true.
|
||||
|
||||
- The [](#opt-programs.yazi.shellWrapperName) option now defaults to
|
||||
`y` instead of `yy`. For users with older `home.stateVersion` values,
|
||||
the legacy default `yy` is retained.
|
||||
- The [](#opt-programs.yazi.shellWrapperName) option now defaults to `y` instead
|
||||
of `yy`.
|
||||
|
||||
- The [](#opt-xdg.userDirs.setSessionVariables) option now defaults to `false`
|
||||
instead of `true`.
|
||||
@@ -51,21 +151,39 @@ changes are only active if the `home.stateVersion` option is set to
|
||||
`DESKTOP`. Home Manager 26.05 introduced a warning when the `XDG_<name>_DIR`
|
||||
form is used.
|
||||
|
||||
- The [](#opt-programs.man.package) option now defaults to `null` on
|
||||
Darwin because the GNU `man` from nixpkgs ships `apropos`/`man -k`
|
||||
and `whatis`/`man -f` binaries that don't work on Darwin. Nix-installed
|
||||
manual pages still work with macOS's built-in `man` via
|
||||
- The [](#opt-programs.man.package) option now defaults to `null` on Darwin
|
||||
because the GNU `man` from nixpkgs ships `apropos`/`man -k` and
|
||||
`whatis`/`man -f` binaries that don't work on Darwin. Nix-installed manual
|
||||
pages still work with macOS's built-in `man` via
|
||||
[](#opt-home.extraOutputsToInstall).
|
||||
|
||||
- The options [](#opt-programs.neovim.withPython3) and
|
||||
[](#opt-programs.neovim.withRuby) now default to `false` following nixpkgs
|
||||
[](#opt-programs.neovim.withRuby) now default to `false` following nixpkgs.
|
||||
|
||||
- Neovim plugin `config` fragments are assumed to be Lua by default through
|
||||
`programs.neovim.plugins.<name>.type = "lua"`, instead of the previous `viml`
|
||||
default.
|
||||
|
||||
- On Linux, `programs.firefox.configPath` now defaults to
|
||||
`"${config.xdg.configHome}/mozilla/firefox"` for `home.stateVersion = "26.05"` and later.
|
||||
Older state versions keep the legacy default of `".mozilla/firefox"`.
|
||||
`"${config.xdg.configHome}/mozilla/firefox"`, instead of the legacy
|
||||
`".mozilla/firefox"`.
|
||||
|
||||
- The [](#opt-wayland.windowManager.hyprland.configType) option now
|
||||
defaults to `"lua"` for `home.stateVersion = "26.05"` and later.
|
||||
Older state versions keep the legacy `"hyprlang"` default. Set
|
||||
`wayland.windowManager.hyprland.configType = "hyprlang"` to keep
|
||||
generating {file}`$XDG_CONFIG_HOME/hypr/hyprland.conf`.
|
||||
- The [](#opt-wayland.windowManager.hyprland.configType) option now defaults to
|
||||
`"lua"`, instead of the legacy `"hyprlang"`. Set
|
||||
`wayland.windowManager.hyprland.configType = "hyprlang"` to keep generating
|
||||
{file}`$XDG_CONFIG_HOME/hypr/hyprland.conf`.
|
||||
|
||||
- The [](#opt-programs.docker-cli.configDir) option now defaults to
|
||||
`"${config.xdg.configHome}/docker"` when [](#opt-xdg.enable) is true, instead
|
||||
of `~/.docker`. Similarly, [](#opt-services.colima.profiles) now sets
|
||||
`$DOCKER_HOST` for the default context.
|
||||
|
||||
- The [](#opt-services.home-manager.autoUpgrade.preSwitchCommands) option now
|
||||
defaults to an empty list. Previously, when
|
||||
[](#opt-services.home-manager.autoUpgrade.useFlake) was `true`, it defaulted
|
||||
to `[ "nix flake update" ]`. Set `preSwitchCommands = [ "nix flake update" ]`
|
||||
to keep updating flake inputs before each automatic switch.
|
||||
|
||||
- The [](#opt-programs.mergiraf.enableGitIntegration) and
|
||||
[](#opt-programs.mergiraf.enableJujutsuIntegration) options now default to
|
||||
`false`, so the integrations must be enabled explicitly.
|
||||
|
||||
Generated
+4
-4
@@ -2,16 +2,16 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1779508470,
|
||||
"narHash": "sha256-Ap9KJX+5xHIn3bPIpfNgT6MEXdAECECwo4/rmlQD74M=",
|
||||
"lastModified": 1784280462,
|
||||
"narHash": "sha256-DtoqIqM7VkR6NxAkcLpMwmi02USwWb3JdmNGLyhthc0=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "29916453413845e54a65b8a1cf996842300cd299",
|
||||
"rev": "293d6abedf0478e681a4dfcfcb35b30fc796a32f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"ref": "nixos-26.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
description = "Home Manager for Nix";
|
||||
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
|
||||
|
||||
outputs =
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@ rec {
|
||||
git = import ./git.nix { inherit lib; };
|
||||
gvariant = import ./gvariant.nix { inherit lib; };
|
||||
maintainers = import ./maintainers.nix;
|
||||
mcp = import ./mcp.nix { inherit lib; };
|
||||
strings = import ./strings.nix { inherit lib; };
|
||||
types = import ./types.nix { inherit gvariant lib; };
|
||||
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
{ lib }:
|
||||
|
||||
let
|
||||
/*
|
||||
Checks whether a value is a file reference submodule.
|
||||
|
||||
A file reference has the shape `{ file = "/path/to/file"; }` and is used
|
||||
to indicate that an environment variable's value should be read from a file
|
||||
at runtime (e.g. via sops-nix or systemd credentials).
|
||||
|
||||
Type: isFileRef :: Value -> Bool
|
||||
*/
|
||||
isFileRef = value: lib.isAttrs value && value ? file;
|
||||
|
||||
/*
|
||||
Filters an `env` attrset to contain only literal (non-file-ref) values.
|
||||
|
||||
This is useful when a transform needs to produce two separate outputs:
|
||||
one with the literal values and one with the file paths (for wrapping).
|
||||
|
||||
Type: literalEnv :: AttrSet -> AttrSet
|
||||
|
||||
Example:
|
||||
literalEnv { API_KEY = "secret"; TOKEN.file = "/run/secrets/token"; }
|
||||
=> { API_KEY = "secret"; }
|
||||
*/
|
||||
literalEnv = env: lib.filterAttrs (_: value: !isFileRef value) env;
|
||||
|
||||
/*
|
||||
Extracts only the file paths from file-ref entries in an `env` attrset.
|
||||
|
||||
Returns a flat `attrsOf str` mapping variable names to their file paths.
|
||||
Non-file-ref entries are excluded.
|
||||
|
||||
Type: fileRefEnv :: AttrSet -> AttrSet
|
||||
|
||||
Example:
|
||||
fileRefEnv { API_KEY = "secret"; TOKEN.file = "/run/secrets/token"; }
|
||||
=> { TOKEN = "/run/secrets/token"; }
|
||||
*/
|
||||
fileRefEnv = env: lib.mapAttrs (_: value: value.file) (lib.filterAttrs (_: isFileRef) env);
|
||||
|
||||
/*
|
||||
Render an `env` attrset (literals + file refs) as a flat `attrsOf str`
|
||||
by mapping file refs through `mkFileRef path` and leaving literals untouched.
|
||||
|
||||
Type: renderEnv :: (String -> String) -> AttrSet -> AttrSet
|
||||
|
||||
Example:
|
||||
renderEnv (path: "{file:${path}}") {
|
||||
API_KEY = "literal";
|
||||
SESSION_TOKEN.file = "/run/secrets/token";
|
||||
}
|
||||
=> { API_KEY = "literal"; SESSION_TOKEN = "{file:/run/secrets/token}"; }
|
||||
*/
|
||||
renderEnv =
|
||||
mkFileRef: env:
|
||||
lib.mapAttrs (_: value: if isFileRef value then mkFileRef value.file else value) env;
|
||||
|
||||
/*
|
||||
Wrap a local MCP server command in a shell script that reads file-backed
|
||||
env values into the environment before executing the original process.
|
||||
Compatible with file-based secret managers such as sops-nix or systemd
|
||||
credentials. Reads file refs from `server.env`.
|
||||
|
||||
Type: mkEnvFilesWrapper :: { pkgs, name, server } -> Derivation
|
||||
*/
|
||||
mkEnvFilesWrapper =
|
||||
{
|
||||
pkgs,
|
||||
name,
|
||||
server,
|
||||
}:
|
||||
let
|
||||
files = fileRefEnv (server.env or { });
|
||||
in
|
||||
pkgs.writeShellScript "mcp-${name}-wrapper" ''
|
||||
${lib.concatStrings (
|
||||
lib.mapAttrsToList (var: path: ''
|
||||
if ${var}=$(cat ${lib.escapeShellArg path}); then
|
||||
export ${var}
|
||||
else
|
||||
printf '[${name} wrapper ] Failed to read env var %s from %s\n' \
|
||||
${lib.escapeShellArg var} \
|
||||
${lib.escapeShellArg path} >&2
|
||||
fi
|
||||
'') files
|
||||
)}
|
||||
exec ${lib.escapeShellArgs ([ server.command ] ++ (server.args or [ ]))}
|
||||
'';
|
||||
|
||||
/*
|
||||
extraTransform factory: when the server has file refs in `env` and a
|
||||
local `command`, replace `command` with a wrapper script that reads
|
||||
those files at startup, reset `args` to `[ ]`, and drop the file refs
|
||||
from `env` (leaving only literal entries).
|
||||
|
||||
When there is nothing to wrap, returns the server unchanged.
|
||||
|
||||
Type: wrapEnvFilesCommand :: { pkgs, name } -> AttrSet -> AttrSet
|
||||
*/
|
||||
wrapEnvFilesCommand =
|
||||
{ pkgs, name }:
|
||||
server:
|
||||
let
|
||||
files = fileRefEnv (server.env or { });
|
||||
needsWrapping = files != { };
|
||||
in
|
||||
server
|
||||
// lib.optionalAttrs needsWrapping {
|
||||
command = mkEnvFilesWrapper { inherit pkgs name server; };
|
||||
args = [ ];
|
||||
env = literalEnv (server.env or { });
|
||||
};
|
||||
|
||||
/*
|
||||
Resolve the effective `enabled` state from a server config that may
|
||||
have either an `enabled` or a `disabled` field (but not both).
|
||||
|
||||
Returns `null` if neither field is present, allowing callers to omit
|
||||
the attribute from the output via the empty-value filter.
|
||||
|
||||
Type: resolveEnabled :: AttrSet -> Null | Bool
|
||||
*/
|
||||
resolveEnabled =
|
||||
server:
|
||||
let
|
||||
hasEnabled = server ? enabled && server.enabled != null;
|
||||
hasDisabled = server ? disabled && server.disabled != null;
|
||||
in
|
||||
if hasEnabled then
|
||||
server.enabled
|
||||
else if hasDisabled then
|
||||
!server.disabled
|
||||
else
|
||||
null;
|
||||
|
||||
/*
|
||||
extraTransform that adds `type = "stdio" | "http"` based on whether the
|
||||
server has a `url`.
|
||||
|
||||
Type: addType :: AttrSet -> AttrSet
|
||||
*/
|
||||
addType =
|
||||
server:
|
||||
if server ? type then
|
||||
server
|
||||
else
|
||||
server
|
||||
// {
|
||||
type = if server ? url && server.url != null then "http" else "stdio";
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit
|
||||
renderEnv
|
||||
mkEnvFilesWrapper
|
||||
wrapEnvFilesCommand
|
||||
addType
|
||||
;
|
||||
|
||||
/*
|
||||
Normalise an MCP server attribute set for consumption by a client
|
||||
program. Performs only universal steps:
|
||||
|
||||
1. Resolve `enabled` from optional `enabled`/`disabled` fields.
|
||||
2. Apply each function in `extraTransforms` to the server attrs, in
|
||||
order. Transforms run before `mkFileRef`, `exclude` and the
|
||||
empty-value filter, so a transform can still read attrs that will
|
||||
be excluded and operate on the raw env shape (with file refs).
|
||||
3. Render any remaining file refs in `env` through `mkFileRef path`.
|
||||
Callers that consume file refs themselves (e.g. via
|
||||
`wrapEnvFilesCommand`) will have stripped them in an
|
||||
extraTransform, so `mkFileRef` is a no-op for them.
|
||||
4. Remove `disabled` and any keys listed in `exclude`.
|
||||
5. Filter `null`, `[]`, and `{}` values.
|
||||
|
||||
Type: transformMcpServer ::
|
||||
{ server, exclude?, extraTransforms?, mkFileRef? }
|
||||
-> AttrSet
|
||||
*/
|
||||
transformMcpServer =
|
||||
{
|
||||
server,
|
||||
exclude ? [ ],
|
||||
extraTransforms ? [ ],
|
||||
mkFileRef ? (path: "{file:${path}}"),
|
||||
}:
|
||||
let
|
||||
enabled = resolveEnabled server;
|
||||
normalised = server // {
|
||||
inherit enabled;
|
||||
url = if (server.url or null) != null then server.url else server.serverUrl or null;
|
||||
};
|
||||
transformed = lib.foldl' (acc: transform: transform acc) normalised extraTransforms;
|
||||
withRenderedEnv =
|
||||
transformed
|
||||
// lib.optionalAttrs (transformed ? env) {
|
||||
env = renderEnv mkFileRef transformed.env;
|
||||
};
|
||||
cleaned = removeAttrs withRenderedEnv (
|
||||
[
|
||||
"disabled"
|
||||
"serverUrl"
|
||||
]
|
||||
++ exclude
|
||||
);
|
||||
in
|
||||
lib.filterAttrs (_: value: value != null && value != [ ] && value != { }) cleaned;
|
||||
}
|
||||
@@ -87,4 +87,11 @@ rec {
|
||||
# Returns true for strings like `SCREAMING_SNAKE_CASE`, `SOME_CONSTANT`.
|
||||
# Must be all uppercase letters/numbers, with words separated by single underscores.
|
||||
isScreamingSnakeCase = str: builtins.match "^[A-Z0-9]+(_[A-Z0-9]+)*$" str != null;
|
||||
|
||||
# Check if the content is path-like (a path, a Nix store path string, or a derivation).
|
||||
isPathLike =
|
||||
content:
|
||||
lib.isPath content
|
||||
|| (builtins.isString content && lib.hasPrefix "${builtins.storeDir}/" content)
|
||||
|| lib.isDerivation content;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{ config, ... }:
|
||||
{
|
||||
time = "2026-03-13T03:09:51+00:00";
|
||||
condition = config.programs.gemini-cli.enable;
|
||||
condition = config.programs.antigravity-cli.enable;
|
||||
message = ''
|
||||
The `programs.gemini-cli.policies` option has been added to support configuring
|
||||
the Gemini CLI policy engine.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{ config, ... }:
|
||||
{
|
||||
time = "2026-03-31T17:22:36+00:00";
|
||||
condition = config.programs.gemini-cli.enable;
|
||||
condition = config.programs.antigravity-cli.enable;
|
||||
message = ''
|
||||
The `programs.gemini-cli.enableMcpIntegration` and `programs.gemini-cli.skills`
|
||||
options have been added to support configuring the Gemini CLI MCP servers and
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
config.programs.claude-code.enable
|
||||
|| config.programs.codex.enable
|
||||
|| config.programs.opencode.enable
|
||||
|| config.programs.gemini-cli.enable;
|
||||
|| config.programs.antigravity-cli.enable;
|
||||
message = ''
|
||||
The Claude Code, Codex, and OpenCode modules now expose a canonical
|
||||
`context` option for configuring their global assistant instructions.
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{ config, ... }:
|
||||
{
|
||||
time = "2026-05-20T12:00:00+00:00";
|
||||
condition = config.programs.antigravity-cli.enable;
|
||||
message = ''
|
||||
The `programs.gemini-cli` module has been renamed to
|
||||
`programs.antigravity-cli`.
|
||||
|
||||
Existing `programs.gemini-cli` configurations are migrated automatically
|
||||
and emit rename warnings. The Antigravity CLI module writes settings and
|
||||
permissions to `~/.gemini/antigravity-cli`, MCP server configuration to
|
||||
`~/.gemini/config/mcp_config.json`, and skills to
|
||||
`~/.gemini/config/skills`.
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{ config, ... }:
|
||||
{
|
||||
time = "2026-05-22T20:55:03+00:00";
|
||||
condition = config.programs.mcp.enable;
|
||||
message = ''
|
||||
The {option}`programs.mcp.servers` schema is now typed.
|
||||
|
||||
Instead of freeform JSON objects, structured options are used:
|
||||
{option}`command` and {option}`args` for local servers,
|
||||
{option}`url` for remote servers,
|
||||
{option}`env` for environment variables.
|
||||
|
||||
Environment variables can be specified as literal strings or as file
|
||||
references that are read at startup:
|
||||
|
||||
```nix
|
||||
env.API_KEY = "literal";
|
||||
env.SESSION_TOKEN.file = "/run/secrets/token";
|
||||
```
|
||||
|
||||
The {var}`lib.hm.mcp` library provides helpers for transforming MCP
|
||||
server configurations and is used by opencode, claude-code, codex,
|
||||
antigravity-cli, zed-editor, and vscode for MCP integration.
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,530 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.programs.antigravity-cli;
|
||||
|
||||
jsonFormat = pkgs.formats.json { };
|
||||
tomlFormat = pkgs.formats.toml { };
|
||||
|
||||
transformMcpServer =
|
||||
server:
|
||||
removeAttrs server [
|
||||
"httpUrl"
|
||||
"url"
|
||||
]
|
||||
// lib.optionalAttrs (server ? httpUrl) {
|
||||
serverUrl = server.httpUrl;
|
||||
}
|
||||
// lib.optionalAttrs (server ? url) {
|
||||
serverUrl = server.url;
|
||||
};
|
||||
|
||||
transformMcpServers = lib.mapAttrs (_name: transformMcpServer);
|
||||
|
||||
commandSkillName = lib.replaceStrings [ "/" ] [ ":" ];
|
||||
|
||||
in
|
||||
{
|
||||
meta.maintainers = [ lib.maintainers.rrvsh ];
|
||||
|
||||
imports = [
|
||||
(lib.mkRenamedOptionModule [ "programs" "gemini-cli" ] [ "programs" "antigravity-cli" ])
|
||||
];
|
||||
|
||||
options.programs.antigravity-cli = {
|
||||
enable = lib.mkEnableOption "Antigravity CLI";
|
||||
|
||||
package = lib.mkPackageOption pkgs "antigravity-cli" {
|
||||
nullable = true;
|
||||
};
|
||||
|
||||
useLegacyGeminiConfig = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Whether to write configuration to the legacy Gemini CLI locations.
|
||||
|
||||
This is enabled automatically when
|
||||
{option}`programs.antigravity-cli.package` is a `gemini-cli` package.
|
||||
'';
|
||||
};
|
||||
|
||||
enableMcpIntegration = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Whether to integrate the MCP servers config from
|
||||
{option}`programs.mcp.servers` into
|
||||
{option}`programs.antigravity-cli.mcpServers`.
|
||||
|
||||
Note: Any servers already present in
|
||||
{option}`programs.antigravity-cli.mcpServers`
|
||||
is not overridden by servers present under the same
|
||||
name in {option}`programs.mcp.servers`
|
||||
'';
|
||||
};
|
||||
|
||||
settings = lib.mkOption {
|
||||
inherit (jsonFormat) type;
|
||||
default = { };
|
||||
example = {
|
||||
colorScheme = "tokyo night";
|
||||
altScreenMode = "always";
|
||||
toolPermission = "proceed-in-sandbox";
|
||||
artifactReviewPolicy = "agent-decides";
|
||||
};
|
||||
description = ''
|
||||
Configuration written to
|
||||
{file}`~/.gemini/antigravity-cli/settings.json`.
|
||||
'';
|
||||
};
|
||||
|
||||
mcpServers = lib.mkOption {
|
||||
inherit (jsonFormat) type;
|
||||
default = { };
|
||||
example = {
|
||||
github = {
|
||||
serverUrl = "https://api.githubcopilot.com/mcp/";
|
||||
};
|
||||
filesystem = {
|
||||
command = "npx";
|
||||
args = [
|
||||
"-y"
|
||||
"@modelcontextprotocol/server-filesystem"
|
||||
"/tmp"
|
||||
];
|
||||
};
|
||||
};
|
||||
description = ''
|
||||
MCP server configuration written to
|
||||
{file}`~/.gemini/config/mcp_config.json`.
|
||||
|
||||
Remote servers use `serverUrl`; `url` and `httpUrl` are accepted
|
||||
for migration and converted automatically.
|
||||
'';
|
||||
};
|
||||
|
||||
commands =
|
||||
let
|
||||
commandType = lib.types.submodule {
|
||||
freeformType = lib.types.str;
|
||||
options = {
|
||||
prompt = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = ''
|
||||
The prompt that will be sent to the Gemini model when the command is executed.
|
||||
This can be a single-line or multi-line string.
|
||||
The special placeholder {{args}} will be replaced with the text the user typed after the command name.
|
||||
'';
|
||||
};
|
||||
description = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = ''
|
||||
A brief, one-line description of what the command does.
|
||||
This text will be displayed next to your command in the /help menu.
|
||||
If you omit this field, a generic description will be generated from the filename.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
lib.mkOption {
|
||||
type = lib.types.attrsOf commandType;
|
||||
default = { };
|
||||
description = ''
|
||||
An attribute set of Gemini CLI custom commands to migrate to
|
||||
Antigravity CLI global skills.
|
||||
|
||||
The name of the attribute set will be the name of each generated
|
||||
skill directory.
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
changelog = {
|
||||
prompt =
|
||||
'''
|
||||
Your task is to parse the `<version>`, `<change_type>`, and `<message>` from their input and use the `write_file` tool to correctly update the `CHANGELOG.md` file.
|
||||
''';
|
||||
description = "Adds a new entry to the project's CHANGELOG.md file.";
|
||||
};
|
||||
"git/fix" = { # Becomes /git:fix
|
||||
prompt = "Please analyze the staged git changes and provide a code fix for the issue described here: {{args}}.";
|
||||
description = "Generates a fix for a given GitHub issue.";
|
||||
};
|
||||
'';
|
||||
};
|
||||
|
||||
permissions = lib.mkOption {
|
||||
type = lib.types.nullOr (
|
||||
lib.types.submodule {
|
||||
options = {
|
||||
allow = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
description = ''
|
||||
Permissions to allow without prompting.
|
||||
'';
|
||||
};
|
||||
|
||||
deny = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
description = ''
|
||||
Permissions to deny without prompting.
|
||||
'';
|
||||
};
|
||||
|
||||
ask = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
description = ''
|
||||
Permissions to ask before using.
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
default = null;
|
||||
description = ''
|
||||
Antigravity CLI fine-grained permissions written to
|
||||
{option}`programs.antigravity-cli.settings.permissions`.
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
allow = [ "command(git)" ];
|
||||
deny = [ "command(rm -rf)" ];
|
||||
ask = [ "command(*)" ];
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
policies = lib.mkOption {
|
||||
type = lib.types.attrsOf (lib.types.either lib.types.path tomlFormat.type);
|
||||
default = { };
|
||||
description = ''
|
||||
An attribute set of Gemini CLI policy definitions to create in
|
||||
{file}`~/.gemini/policies/` when
|
||||
{option}`programs.antigravity-cli.package` is a `gemini-cli`
|
||||
package.
|
||||
|
||||
Antigravity CLI configures permissions in
|
||||
{option}`programs.antigravity-cli.settings.permissions` or
|
||||
{option}`programs.antigravity-cli.permissions`.
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
"my-rules" = {
|
||||
rule = [
|
||||
{
|
||||
toolName = "run_shell_command";
|
||||
commandPrefix = "git ";
|
||||
decision = "ask_user";
|
||||
priority = 100;
|
||||
}
|
||||
];
|
||||
};
|
||||
"other-rules" = ./path/to/rules.toml;
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
defaultModel = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "gemini-2.5-flash";
|
||||
description = ''
|
||||
The default model to use for the CLI.
|
||||
Will be set as $GEMINI_MODEL when configured.
|
||||
'';
|
||||
};
|
||||
|
||||
context = lib.mkOption {
|
||||
type = lib.types.attrsOf (lib.types.either lib.types.lines lib.types.path);
|
||||
default = { };
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
GEMINI = '''
|
||||
# Global Context
|
||||
|
||||
You are a helpful AI assistant for software development.
|
||||
|
||||
## Coding Standards
|
||||
|
||||
- Follow consistent code style
|
||||
- Write clear comments
|
||||
- Test your changes
|
||||
''';
|
||||
|
||||
AGENTS = ./path/to/agents.md;
|
||||
|
||||
CONTEXT = '''
|
||||
Additional context instructions here.
|
||||
''';
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Global context(s) for Antigravity CLI.
|
||||
|
||||
The attribute name becomes the filename, with a {file}`.md`
|
||||
extension added automatically. Each value is either:
|
||||
- Inline content as a string
|
||||
- A path to a file containing the content
|
||||
|
||||
The configured files are written to {file}`~/.gemini/`.
|
||||
|
||||
Note: You can customize which context file names Antigravity CLI looks for by setting
|
||||
`settings.context.fileName`. For example:
|
||||
```nix
|
||||
settings = {
|
||||
context.fileName = ["AGENTS.md", "CONTEXT.md", "GEMINI.md"];
|
||||
};
|
||||
```
|
||||
'';
|
||||
};
|
||||
|
||||
skills = lib.mkOption {
|
||||
type = lib.types.either (lib.types.attrsOf (lib.types.either lib.types.lines lib.types.path)) lib.types.path;
|
||||
default = { };
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
xlsx = ./skills/xlsx/SKILL.md;
|
||||
data-analysis = ./skills/data-analysis;
|
||||
pdf-processing = '''
|
||||
---
|
||||
name: pdf-processing
|
||||
description: Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDF files or when the user mentions PDFs, forms, or document extraction.
|
||||
---
|
||||
|
||||
# PDF Processing
|
||||
|
||||
## Quick start
|
||||
|
||||
Use pdfplumber to extract text from PDFs:
|
||||
|
||||
```python
|
||||
import pdfplumber
|
||||
|
||||
with pdfplumber.open("document.pdf") as pdf:
|
||||
text = pdf.pages[0].extract_text()
|
||||
```
|
||||
''';
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Custom skills for Antigravity CLI.
|
||||
|
||||
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 `~/.gemini/config/skills/<name>/SKILL.md`)
|
||||
- A path to a file (creates `~/.gemini/config/skills/<name>/SKILL.md`)
|
||||
- A path to a directory (symlinks `~/.gemini/config/skills/<name>/` to that directory)
|
||||
|
||||
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}`~/.gemini/config/skills/`.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config =
|
||||
let
|
||||
useGeminiConfig =
|
||||
cfg.useLegacyGeminiConfig || (cfg.package != null && lib.getName cfg.package == "gemini-cli");
|
||||
antigravitySkillsDir = ".gemini/config/skills";
|
||||
geminiSkillsDir = ".gemini/skills";
|
||||
antigravitySettings = lib.recursiveUpdate (lib.optionalAttrs (cfg.permissions != null) {
|
||||
inherit (cfg) permissions;
|
||||
}) (removeAttrs cfg.settings [ "mcpServers" ]);
|
||||
legacyMcpServers = lib.optionalAttrs (cfg.settings ? mcpServers) cfg.settings.mcpServers;
|
||||
mcpServers = lib.recursiveUpdate legacyMcpServers cfg.mcpServers;
|
||||
geminiSettings =
|
||||
lib.recursiveUpdate
|
||||
(lib.optionalAttrs (cfg.permissions != null) {
|
||||
inherit (cfg) permissions;
|
||||
})
|
||||
(lib.recursiveUpdate cfg.settings (lib.optionalAttrs (mcpServers != { }) { inherit mcpServers; }));
|
||||
in
|
||||
lib.mkIf cfg.enable (
|
||||
lib.mkMerge [
|
||||
{
|
||||
home = {
|
||||
packages = lib.mkIf (cfg.package != null) [ cfg.package ];
|
||||
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;
|
||||
}
|
||||
(lib.mkIf useGeminiConfig {
|
||||
programs.antigravity-cli.settings.mcpServers =
|
||||
lib.mkIf (cfg.enableMcpIntegration && config.programs.mcp.enable)
|
||||
(
|
||||
lib.mapAttrs (
|
||||
name: server:
|
||||
lib.hm.mcp.transformMcpServer {
|
||||
inherit server;
|
||||
extraTransforms = [
|
||||
lib.hm.mcp.addType
|
||||
(lib.hm.mcp.wrapEnvFilesCommand { inherit pkgs name; })
|
||||
(s: lib.mapAttrs (_: lib.mkDefault) s)
|
||||
];
|
||||
}
|
||||
) config.programs.mcp.servers
|
||||
);
|
||||
|
||||
home.file =
|
||||
lib.optionalAttrs (geminiSettings != { }) {
|
||||
".gemini/settings.json".source = jsonFormat.generate "gemini-cli-settings.json" geminiSettings;
|
||||
}
|
||||
// lib.mapAttrs' (
|
||||
n: v:
|
||||
lib.nameValuePair ".gemini/commands/${n}.toml" {
|
||||
source = tomlFormat.generate "gemini-cli-command-${n}.toml" {
|
||||
inherit (v) description prompt;
|
||||
};
|
||||
}
|
||||
) cfg.commands
|
||||
// lib.mapAttrs' (
|
||||
n: v:
|
||||
lib.nameValuePair ".gemini/policies/${n}.toml" {
|
||||
source =
|
||||
if lib.hm.strings.isPathLike v then v else tomlFormat.generate "gemini-cli-policy-${n}.toml" v;
|
||||
}
|
||||
) cfg.policies
|
||||
// (
|
||||
if lib.hm.strings.isPathLike cfg.skills then
|
||||
{
|
||||
"${geminiSkillsDir}" = {
|
||||
source = cfg.skills;
|
||||
recursive = true;
|
||||
};
|
||||
}
|
||||
else
|
||||
lib.mapAttrs' (
|
||||
n: v:
|
||||
if lib.hm.strings.isPathLike v && lib.pathIsDirectory v then
|
||||
lib.nameValuePair "${geminiSkillsDir}/${n}" {
|
||||
source = v;
|
||||
recursive = true;
|
||||
}
|
||||
else
|
||||
lib.nameValuePair "${geminiSkillsDir}/${n}/SKILL.md" (
|
||||
if lib.hm.strings.isPathLike v then { source = v; } else { text = v; }
|
||||
)
|
||||
) cfg.skills
|
||||
);
|
||||
})
|
||||
(lib.mkIf (!useGeminiConfig) {
|
||||
programs.antigravity-cli.mcpServers =
|
||||
lib.mkIf (cfg.enableMcpIntegration && config.programs.mcp.enable)
|
||||
(
|
||||
lib.mapAttrs (
|
||||
name: server:
|
||||
lib.hm.mcp.transformMcpServer {
|
||||
inherit server;
|
||||
extraTransforms = [
|
||||
(
|
||||
s:
|
||||
removeAttrs s [
|
||||
"httpUrl"
|
||||
"url"
|
||||
]
|
||||
// lib.optionalAttrs (s ? httpUrl) {
|
||||
serverUrl = s.httpUrl;
|
||||
}
|
||||
// lib.optionalAttrs (s ? url) {
|
||||
serverUrl = s.url;
|
||||
}
|
||||
)
|
||||
lib.hm.mcp.addType
|
||||
(lib.hm.mcp.wrapEnvFilesCommand { inherit pkgs name; })
|
||||
(s: lib.mapAttrs (_: lib.mkDefault) s)
|
||||
];
|
||||
}
|
||||
) config.programs.mcp.servers
|
||||
);
|
||||
|
||||
assertions = [
|
||||
{
|
||||
assertion = cfg.policies == { };
|
||||
message = ''
|
||||
`programs.antigravity-cli.policies` is only supported when
|
||||
`programs.antigravity-cli.package` is a `gemini-cli` package.
|
||||
Antigravity CLI configures permissions in
|
||||
`programs.antigravity-cli.settings.permissions` or
|
||||
`programs.antigravity-cli.permissions`.
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
home.file =
|
||||
lib.optionalAttrs (antigravitySettings != { }) {
|
||||
".gemini/antigravity-cli/settings.json".source =
|
||||
jsonFormat.generate "antigravity-cli-settings.json" antigravitySettings;
|
||||
}
|
||||
// lib.optionalAttrs (mcpServers != { }) {
|
||||
".gemini/config/mcp_config.json".source = jsonFormat.generate "antigravity-cli-mcp-config.json" {
|
||||
mcpServers = transformMcpServers mcpServers;
|
||||
};
|
||||
}
|
||||
// lib.mapAttrs' (
|
||||
n: v:
|
||||
let
|
||||
skillName = commandSkillName n;
|
||||
in
|
||||
lib.nameValuePair "${antigravitySkillsDir}/${skillName}/SKILL.md" {
|
||||
text = ''
|
||||
---
|
||||
name: ${skillName}
|
||||
description: ${v.description}
|
||||
---
|
||||
|
||||
${v.prompt}
|
||||
'';
|
||||
}
|
||||
) cfg.commands
|
||||
// (
|
||||
if lib.hm.strings.isPathLike cfg.skills then
|
||||
{
|
||||
"${antigravitySkillsDir}" = {
|
||||
source = cfg.skills;
|
||||
recursive = true;
|
||||
};
|
||||
}
|
||||
else
|
||||
lib.mapAttrs' (
|
||||
n: v:
|
||||
if lib.hm.strings.isPathLike v && lib.pathIsDirectory v then
|
||||
lib.nameValuePair "${antigravitySkillsDir}/${n}" {
|
||||
source = v;
|
||||
recursive = true;
|
||||
}
|
||||
else
|
||||
lib.nameValuePair "${antigravitySkillsDir}/${n}/SKILL.md" (
|
||||
if lib.hm.strings.isPathLike v then { source = v; } else { text = v; }
|
||||
)
|
||||
) cfg.skills
|
||||
);
|
||||
})
|
||||
{
|
||||
assertions = [
|
||||
{
|
||||
assertion = !lib.hm.strings.isPathLike cfg.skills || lib.pathIsDirectory cfg.skills;
|
||||
message = "`programs.antigravity-cli.skills` must be a directory when set to a path";
|
||||
}
|
||||
];
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -19,17 +19,17 @@ let
|
||||
|
||||
upstreamConfigDir = "${config.home.homeDirectory}/.claude";
|
||||
|
||||
mkMcpServer =
|
||||
server:
|
||||
(removeAttrs server [ "disabled" ])
|
||||
// (optionalAttrs (server ? url) { type = "http"; })
|
||||
// (optionalAttrs (server ? command) { type = "stdio"; })
|
||||
// {
|
||||
enabled = !(server.disabled or false);
|
||||
};
|
||||
|
||||
transformedMcpServers = optionalAttrs (cfg.enableMcpIntegration && config.programs.mcp.enable) (
|
||||
lib.mapAttrs (_name: mkMcpServer) config.programs.mcp.servers
|
||||
transformedMcpServers = lib.optionalAttrs (cfg.enableMcpIntegration && config.programs.mcp.enable) (
|
||||
lib.mapAttrs (
|
||||
name: server:
|
||||
lib.hm.mcp.transformMcpServer {
|
||||
inherit server;
|
||||
extraTransforms = [
|
||||
lib.hm.mcp.addType
|
||||
(lib.hm.mcp.wrapEnvFilesCommand { inherit pkgs name; }) # envFiles currently still need wrapping https://github.com/anthropics/claude-code/issues/28942
|
||||
];
|
||||
}
|
||||
) config.programs.mcp.servers
|
||||
);
|
||||
|
||||
mkContentOption =
|
||||
@@ -548,20 +548,20 @@ in
|
||||
let
|
||||
mkSourceEntry = content: if lib.isPath content then { source = content; } else { text = content; };
|
||||
|
||||
isStorePathString =
|
||||
content: builtins.isString content && lib.hasPrefix "${builtins.storeDir}/" content;
|
||||
isPathLikeContent = content: lib.isPath content || isStorePathString content;
|
||||
|
||||
mkMarkdownEntries =
|
||||
subdir: attrs:
|
||||
lib.mapAttrs' (
|
||||
name: content: nameValuePair "${cfg.configDir}/${subdir}/${name}.md" (mkSourceEntry content)
|
||||
) attrs;
|
||||
|
||||
mkTextEntries =
|
||||
subdir: attrs:
|
||||
mkHookEntries =
|
||||
attrs:
|
||||
lib.mapAttrs' (
|
||||
name: content: nameValuePair "${cfg.configDir}/${subdir}/${name}" { text = content; }
|
||||
name: content:
|
||||
nameValuePair "${cfg.configDir}/hooks/${name}" {
|
||||
text = content;
|
||||
executable = true;
|
||||
}
|
||||
) attrs;
|
||||
|
||||
mkRecursiveDirAttrs =
|
||||
@@ -575,14 +575,14 @@ in
|
||||
|
||||
mkSkillEntry =
|
||||
name: content:
|
||||
if isPathLikeContent content && lib.pathIsDirectory content then
|
||||
if lib.hm.strings.isPathLike content && lib.pathIsDirectory content then
|
||||
nameValuePair "${cfg.configDir}/skills/${name}" {
|
||||
source = content;
|
||||
recursive = true;
|
||||
}
|
||||
else
|
||||
nameValuePair "${cfg.configDir}/skills/${name}/SKILL.md" (
|
||||
if isPathLikeContent content then { source = content; } else { text = content; }
|
||||
if lib.hm.strings.isPathLike content then { source = content; } else { text = content; }
|
||||
);
|
||||
|
||||
mkMarketplaceEntry = _name: content: {
|
||||
@@ -624,7 +624,7 @@ in
|
||||
message = "`programs.claude-code.package` cannot be null when `mcpServers`, `lspServers`, `enableMcpIntegration`, or `plugins` is configured";
|
||||
}
|
||||
{
|
||||
assertion = !isPathLikeContent cfg.skills || lib.pathIsDirectory cfg.skills;
|
||||
assertion = !lib.hm.strings.isPathLike cfg.skills || lib.pathIsDirectory cfg.skills;
|
||||
message = "`programs.claude-code.skills` must be a directory when set to a path";
|
||||
}
|
||||
]
|
||||
@@ -632,7 +632,7 @@ in
|
||||
|
||||
programs.claude-code.finalPackage =
|
||||
let
|
||||
mergedMcpServers = transformedMcpServers // cfg.mcpServers;
|
||||
mergedMcpServers = transformedMcpServers // lib.mapAttrs (_: lib.hm.mcp.addType) cfg.mcpServers;
|
||||
pluginFiles =
|
||||
lib.optional (mergedMcpServers != { }) {
|
||||
name = ".mcp.json";
|
||||
@@ -721,13 +721,13 @@ in
|
||||
(mkRecursiveDirAttrs "commands" cfg.commandsDir)
|
||||
(mkRecursiveDirAttrs "hooks" cfg.hooksDir)
|
||||
(mkRecursiveDirAttrs "rules" cfg.rulesDir)
|
||||
(lib.mkIf (isPathLikeContent cfg.skills) {
|
||||
(lib.mkIf (lib.hm.strings.isPathLike cfg.skills) {
|
||||
"${cfg.configDir}/skills" = {
|
||||
source = cfg.skills;
|
||||
recursive = true;
|
||||
};
|
||||
})
|
||||
(mkTextEntries "hooks" cfg.hooks)
|
||||
(mkHookEntries cfg.hooks)
|
||||
(lib.optionalAttrs (builtins.isAttrs cfg.skills) (lib.mapAttrs' mkSkillEntry cfg.skills))
|
||||
(mkMarkdownEntries "output-styles" cfg.outputStyles)
|
||||
];
|
||||
|
||||
+21
-23
@@ -17,8 +17,8 @@ let
|
||||
settingsFormat = if isTomlConfig then tomlFormat else yamlFormat;
|
||||
in
|
||||
{
|
||||
meta.maintainers = [
|
||||
lib.maintainers.delafthi
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
delafthi
|
||||
];
|
||||
|
||||
imports = [
|
||||
@@ -191,24 +191,21 @@ 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;
|
||||
isPathLikeContent = content: lib.isPath content || isStorePathString content;
|
||||
mkSkillDir =
|
||||
content:
|
||||
pkgs.writeTextDir "SKILL.md" (
|
||||
if isPathLikeContent content then builtins.readFile content else content
|
||||
if lib.hm.strings.isPathLike content then builtins.readFile content else content
|
||||
);
|
||||
skillSources =
|
||||
if builtins.isAttrs cfg.skills then
|
||||
cfg.skills
|
||||
else if isPathLikeContent cfg.skills && lib.pathIsDirectory cfg.skills then
|
||||
else if lib.hm.strings.isPathLike cfg.skills && lib.pathIsDirectory cfg.skills then
|
||||
lib.mapAttrs (name: _type: cfg.skills + "/${name}") (builtins.readDir cfg.skills)
|
||||
else
|
||||
{ };
|
||||
mkSkillEntry =
|
||||
name: content:
|
||||
if isPathLikeContent content && lib.pathIsDirectory content then
|
||||
if lib.hm.strings.isPathLike content && lib.pathIsDirectory content then
|
||||
lib.nameValuePair "${skillsDir}/${name}" {
|
||||
source = content;
|
||||
}
|
||||
@@ -219,26 +216,27 @@ in
|
||||
mkRuleEntry =
|
||||
name: content:
|
||||
lib.nameValuePair "${configDir}/rules/${name}.rules" (
|
||||
if isPathLikeContent content then { source = content; } else { text = content; }
|
||||
if lib.hm.strings.isPathLike content then { source = content; } else { text = content; }
|
||||
);
|
||||
|
||||
transformedMcpServers = lib.optionalAttrs (cfg.enableMcpIntegration && config.programs.mcp.enable) (
|
||||
lib.mapAttrs (
|
||||
_name: server:
|
||||
name: server:
|
||||
# NOTE: Convert shared programs.mcp fields to Codex config keys:
|
||||
# - removeAttrs drops keys that Codex does not use directly
|
||||
# - "disabled" becomes inverse "enabled"
|
||||
# - file-backed env entries are wrapped in a shell script that sets environment variables before exec
|
||||
# - "headers" is renamed to "http_headers"
|
||||
# See: https://developers.openai.com/codex/mcp#other-configuration-options
|
||||
(lib.removeAttrs server [
|
||||
"disabled"
|
||||
"headers"
|
||||
])
|
||||
// (lib.optionalAttrs (server ? headers && !(server ? http_headers)) {
|
||||
http_headers = server.headers;
|
||||
})
|
||||
// {
|
||||
enabled = !(server.disabled or false);
|
||||
lib.hm.mcp.transformMcpServer {
|
||||
inherit server;
|
||||
exclude = [
|
||||
"headers"
|
||||
"type"
|
||||
];
|
||||
extraTransforms = [
|
||||
(s: s // lib.optionalAttrs (s.headers or { } != { }) { http_headers = s.headers; })
|
||||
lib.hm.mcp.addType
|
||||
(lib.hm.mcp.wrapEnvFilesCommand { inherit pkgs name; })
|
||||
];
|
||||
}
|
||||
) config.programs.mcp.servers
|
||||
);
|
||||
@@ -251,11 +249,11 @@ in
|
||||
mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = !isPathLikeContent cfg.skills || lib.pathIsDirectory cfg.skills;
|
||||
assertion = !lib.hm.strings.isPathLike cfg.skills || lib.pathIsDirectory cfg.skills;
|
||||
message = "`programs.codex.skills` must be a directory when set to a path";
|
||||
}
|
||||
{
|
||||
assertion = lib.all (content: !(isPathLikeContent content && lib.pathIsDirectory content)) (
|
||||
assertion = lib.all (content: !(lib.hm.strings.isPathLike content && lib.pathIsDirectory content)) (
|
||||
lib.attrValues cfg.rules
|
||||
);
|
||||
message = "`programs.codex.rules` attribute values must be files when set to paths";
|
||||
|
||||
@@ -1,314 +0,0 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.programs.gemini-cli;
|
||||
|
||||
jsonFormat = pkgs.formats.json { };
|
||||
tomlFormat = pkgs.formats.toml { };
|
||||
in
|
||||
{
|
||||
meta.maintainers = [ lib.maintainers.rrvsh ];
|
||||
|
||||
options.programs.gemini-cli = {
|
||||
enable = lib.mkEnableOption "gemini-cli";
|
||||
|
||||
package = lib.mkPackageOption pkgs "gemini-cli" { nullable = true; };
|
||||
|
||||
enableMcpIntegration = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Whether to integrate the MCP servers config from
|
||||
{option}`programs.mcp.servers` into
|
||||
{option}`programs.gemini-cli.settings.mcpServers`.
|
||||
|
||||
Note: Any servers already present in
|
||||
{option}`programs.gemini-cli.settings.mcpServers`
|
||||
is not overridden by servers present under the same
|
||||
name in {option}`programs.mcp.servers`
|
||||
'';
|
||||
};
|
||||
|
||||
settings = lib.mkOption {
|
||||
inherit (jsonFormat) type;
|
||||
default = { };
|
||||
example = {
|
||||
ui.theme = "Default";
|
||||
general = {
|
||||
vimMode = true;
|
||||
preferredEditor = "nvim";
|
||||
previewFeatures = true;
|
||||
};
|
||||
ide.enabled = true;
|
||||
privacy.usageStatisticsEnabled = false;
|
||||
tools.autoAccept = false;
|
||||
context.loadMemoryFromIncludeDirectories = true;
|
||||
security.auth.selectedType = "oauth-personal";
|
||||
};
|
||||
description = "JSON config for gemini-cli";
|
||||
};
|
||||
|
||||
commands =
|
||||
let
|
||||
commandType = lib.types.submodule {
|
||||
freeformType = lib.types.str;
|
||||
options = {
|
||||
prompt = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = ''
|
||||
The prompt that will be sent to the Gemini model when the command is executed.
|
||||
This can be a single-line or multi-line string.
|
||||
The special placeholder {{args}} will be replaced with the text the user typed after the command name.
|
||||
'';
|
||||
};
|
||||
description = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = ''
|
||||
A brief, one-line description of what the command does.
|
||||
This text will be displayed next to your command in the /help menu.
|
||||
If you omit this field, a generic description will be generated from the filename.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
lib.mkOption {
|
||||
type = lib.types.attrsOf commandType;
|
||||
default = { };
|
||||
description = ''
|
||||
An attribute set of custom commands that will be globally available.
|
||||
The name of the attribute set will be the name of each command.
|
||||
You may use subdirectories to create namespaced commands, such as `git/fix` becoming `/git:fix`.
|
||||
See https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/commands.md#custom-commands for more information.
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
changelog = {
|
||||
prompt =
|
||||
'''
|
||||
Your task is to parse the `<version>`, `<change_type>`, and `<message>` from their input and use the `write_file` tool to correctly update the `CHANGELOG.md` file.
|
||||
''';
|
||||
description = "Adds a new entry to the project's CHANGELOG.md file.";
|
||||
};
|
||||
"git/fix" = { # Becomes /git:fix
|
||||
prompt = "Please analyze the staged git changes and provide a code fix for the issue described here: {{args}}.";
|
||||
description = "Generates a fix for a given GitHub issue.";
|
||||
};
|
||||
'';
|
||||
};
|
||||
|
||||
policies = lib.mkOption {
|
||||
type = lib.types.attrsOf (lib.types.either lib.types.path tomlFormat.type);
|
||||
default = { };
|
||||
description = ''
|
||||
An attribute set of policy definitions to create in `~/.gemini/policies/`.
|
||||
The attribute name becomes the filename with `.toml` extension automatically added.
|
||||
The value can be either an attribute set representing the TOML policy or a path to a TOML file.
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
"my-rules" = {
|
||||
rule = [
|
||||
{
|
||||
toolName = "run_shell_command";
|
||||
commandPrefix = "git ";
|
||||
decision = "ask_user";
|
||||
priority = 100;
|
||||
}
|
||||
];
|
||||
};
|
||||
"other-rules" = ./path/to/rules.toml;
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
defaultModel = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "gemini-2.5-flash";
|
||||
description = ''
|
||||
The default model to use for the CLI.
|
||||
Will be set as $GEMINI_MODEL when configured.
|
||||
'';
|
||||
};
|
||||
|
||||
context = lib.mkOption {
|
||||
type = lib.types.attrsOf (lib.types.either lib.types.lines lib.types.path);
|
||||
default = { };
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
GEMINI = '''
|
||||
# Global Context
|
||||
|
||||
You are a helpful AI assistant for software development.
|
||||
|
||||
## Coding Standards
|
||||
|
||||
- Follow consistent code style
|
||||
- Write clear comments
|
||||
- Test your changes
|
||||
''';
|
||||
|
||||
AGENTS = ./path/to/agents.md;
|
||||
|
||||
CONTEXT = '''
|
||||
Additional context instructions here.
|
||||
''';
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Global context(s) for gemini-cli.
|
||||
|
||||
The attribute name becomes the filename, with a {file}`.md`
|
||||
extension added automatically. Each value is either:
|
||||
- Inline content as a string
|
||||
- A path to a file containing the content
|
||||
|
||||
The configured files are written to {file}`~/.gemini/`.
|
||||
|
||||
Note: You can customize which context file names gemini-cli looks for by setting
|
||||
`settings.context.fileName`. For example:
|
||||
```nix
|
||||
settings = {
|
||||
context.fileName = ["AGENTS.md", "CONTEXT.md", "GEMINI.md"];
|
||||
};
|
||||
```
|
||||
'';
|
||||
};
|
||||
|
||||
skills = lib.mkOption {
|
||||
type = lib.types.either (lib.types.attrsOf (lib.types.either lib.types.lines lib.types.path)) lib.types.path;
|
||||
default = { };
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
xlsx = ./skills/xlsx/SKILL.md;
|
||||
data-analysis = ./skills/data-analysis;
|
||||
pdf-processing = '''
|
||||
---
|
||||
name: pdf-processing
|
||||
description: Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDF files or when the user mentions PDFs, forms, or document extraction.
|
||||
---
|
||||
|
||||
# PDF Processing
|
||||
|
||||
## Quick start
|
||||
|
||||
Use pdfplumber to extract text from PDFs:
|
||||
|
||||
```python
|
||||
import pdfplumber
|
||||
|
||||
with pdfplumber.open("document.pdf") as pdf:
|
||||
text = pdf.pages[0].extract_text()
|
||||
```
|
||||
''';
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Custom skills for gemini-cli.
|
||||
|
||||
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 `~/.gemini/skills/<name>/SKILL.md`)
|
||||
- A path to a file (creates `~/.gemini/skills/<name>/SKILL.md`)
|
||||
- A path to a directory (symlinks `~/.gemini/skills/<name>/` to that directory)
|
||||
|
||||
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}`~/.gemini/skills/`.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
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;
|
||||
};
|
||||
}
|
||||
) 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.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;
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -19,39 +19,57 @@ let
|
||||
|
||||
upstreamConfigDir = "${config.home.homeDirectory}/.copilot";
|
||||
|
||||
isStorePathString =
|
||||
content: builtins.isString content && lib.hasPrefix "${builtins.storeDir}/" content;
|
||||
|
||||
isPathLikeContent = content: lib.isPath content || isStorePathString content;
|
||||
|
||||
transformSingleServer =
|
||||
_name: server:
|
||||
forCopilotFormat =
|
||||
server:
|
||||
let
|
||||
server' = removeAttrs server [ "disabled" ];
|
||||
type = server'.type or (if server' ? url then "http" else "local");
|
||||
isLocal = server.type == "stdio";
|
||||
in
|
||||
server'
|
||||
server
|
||||
// {
|
||||
inherit type;
|
||||
type = if server.type == "stdio" then "local" else server.type or "local";
|
||||
}
|
||||
// lib.optionalAttrs (type == "local") {
|
||||
args = server'.args or [ ];
|
||||
// lib.optionalAttrs isLocal {
|
||||
args = server.args or [ ];
|
||||
}
|
||||
// lib.optionalAttrs (!(server' ? tools)) {
|
||||
// lib.optionalAttrs (!(server ? tools)) {
|
||||
tools = [ "*" ];
|
||||
};
|
||||
|
||||
enabledServers = lib.filterAttrs (
|
||||
_: server: !(server.disabled or false) && (server ? url || server ? command)
|
||||
) config.programs.mcp.servers;
|
||||
|
||||
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
|
||||
)
|
||||
if cfg.enableMcpIntegration && config.programs.mcp.enable && enabledServers != { } then
|
||||
lib.mapAttrs (
|
||||
name: server:
|
||||
lib.hm.mcp.transformMcpServer {
|
||||
inherit server;
|
||||
extraTransforms = [
|
||||
lib.hm.mcp.addType
|
||||
(lib.hm.mcp.wrapEnvFilesCommand { inherit pkgs name; })
|
||||
forCopilotFormat
|
||||
];
|
||||
}
|
||||
) enabledServers
|
||||
else
|
||||
{ };
|
||||
|
||||
mergedMcpServers = transformedMcpServers // cfg.mcpServers;
|
||||
mergedMcpServers =
|
||||
transformedMcpServers
|
||||
// lib.mapAttrs (
|
||||
name: server:
|
||||
lib.hm.mcp.transformMcpServer {
|
||||
inherit server;
|
||||
extraTransforms = [
|
||||
lib.hm.mcp.addType
|
||||
(lib.hm.mcp.wrapEnvFilesCommand {
|
||||
inherit pkgs name;
|
||||
})
|
||||
forCopilotFormat
|
||||
];
|
||||
}
|
||||
) cfg.mcpServers;
|
||||
in
|
||||
{
|
||||
meta.maintainers = [ lib.maintainers.ojsef39 ];
|
||||
@@ -344,11 +362,11 @@ in
|
||||
config = mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = !isPathLikeContent cfg.agents || lib.pathIsDirectory cfg.agents;
|
||||
assertion = !lib.hm.strings.isPathLike cfg.agents || lib.pathIsDirectory cfg.agents;
|
||||
message = "`programs.github-copilot-cli.agents` must be a directory when set to a path";
|
||||
}
|
||||
{
|
||||
assertion = !isPathLikeContent cfg.skills || lib.pathIsDirectory cfg.skills;
|
||||
assertion = !lib.hm.strings.isPathLike cfg.skills || lib.pathIsDirectory cfg.skills;
|
||||
message = "`programs.github-copilot-cli.skills` must be a directory when set to a path";
|
||||
}
|
||||
];
|
||||
@@ -379,19 +397,19 @@ in
|
||||
};
|
||||
|
||||
"${cfg.configDir}/copilot-instructions.md" =
|
||||
if isPathLikeContent cfg.context then
|
||||
if lib.hm.strings.isPathLike cfg.context then
|
||||
{ source = cfg.context; }
|
||||
else
|
||||
mkIf (cfg.context != "") {
|
||||
text = cfg.context;
|
||||
};
|
||||
|
||||
"${cfg.configDir}/agents" = mkIf (isPathLikeContent cfg.agents) {
|
||||
"${cfg.configDir}/agents" = mkIf (lib.hm.strings.isPathLike cfg.agents) {
|
||||
source = cfg.agents;
|
||||
recursive = true;
|
||||
};
|
||||
|
||||
"${cfg.configDir}/skills" = mkIf (isPathLikeContent cfg.skills) {
|
||||
"${cfg.configDir}/skills" = mkIf (lib.hm.strings.isPathLike cfg.skills) {
|
||||
source = cfg.skills;
|
||||
recursive = true;
|
||||
};
|
||||
@@ -400,21 +418,21 @@ in
|
||||
lib.mapAttrs' (
|
||||
name: content:
|
||||
lib.nameValuePair "${cfg.configDir}/agents/${name}.agent.md" (
|
||||
if isPathLikeContent content then { source = content; } else { text = content; }
|
||||
if lib.hm.strings.isPathLike content then { source = content; } else { text = content; }
|
||||
)
|
||||
) cfg.agents
|
||||
)
|
||||
// lib.optionalAttrs (builtins.isAttrs cfg.skills) (
|
||||
lib.mapAttrs' (
|
||||
name: content:
|
||||
if isPathLikeContent content && lib.pathIsDirectory content then
|
||||
if lib.hm.strings.isPathLike content && lib.pathIsDirectory content then
|
||||
lib.nameValuePair "${cfg.configDir}/skills/${name}" {
|
||||
source = content;
|
||||
recursive = true;
|
||||
}
|
||||
else
|
||||
lib.nameValuePair "${cfg.configDir}/skills/${name}/SKILL.md" (
|
||||
if isPathLikeContent content then { source = content; } else { text = content; }
|
||||
if lib.hm.strings.isPathLike content then { source = content; } else { text = content; }
|
||||
)
|
||||
) cfg.skills
|
||||
);
|
||||
|
||||
+162
-21
@@ -13,52 +13,193 @@ let
|
||||
;
|
||||
|
||||
cfg = config.programs.mcp;
|
||||
|
||||
jsonFormat = pkgs.formats.json { };
|
||||
|
||||
serverModule = lib.types.submodule {
|
||||
freeformType = jsonFormat.type;
|
||||
|
||||
options = {
|
||||
command = mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
Executable for a local (stdio) MCP server.
|
||||
Mutually exclusive with {option}`url`.
|
||||
'';
|
||||
example = "npx";
|
||||
};
|
||||
|
||||
args = mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
description = ''
|
||||
Arguments passed to {option}`command`.
|
||||
Only valid for local servers.
|
||||
'';
|
||||
example = [
|
||||
"-y"
|
||||
"@modelcontextprotocol/server-everything"
|
||||
];
|
||||
};
|
||||
|
||||
env = mkOption {
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.oneOf [
|
||||
lib.types.str
|
||||
(lib.types.submodule {
|
||||
options = {
|
||||
file = mkOption {
|
||||
type = lib.types.str;
|
||||
description = ''
|
||||
Path to a file whose content is read at startup.
|
||||
Compatible with file-based secret managers such as sops-nix
|
||||
or systemd credentials.
|
||||
'';
|
||||
};
|
||||
};
|
||||
})
|
||||
]
|
||||
);
|
||||
default = { };
|
||||
description = ''
|
||||
Environment variables set when spawning the MCP server.
|
||||
Each value is either a literal string or a file reference
|
||||
(`{ file = "/path"; }`).
|
||||
|
||||
File references behave differently depending on the consumer:
|
||||
- Clients that understand the `{file:…}` syntax (e.g. opencode)
|
||||
receive the path as a substitution token.
|
||||
- Other clients receive a generated wrapper script that reads
|
||||
the secret at startup instead.
|
||||
|
||||
Only valid for local servers.
|
||||
'';
|
||||
example = literalExpression ''
|
||||
{
|
||||
API_BASE_URL = "https://api.example.com";
|
||||
FORGEJO_ACCESS_TOKEN.file = "/run/secrets/forgejo_token";
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
url = mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
HTTP(S) endpoint for a remote (SSE/HTTP) MCP server.
|
||||
Mutually exclusive with {option}`command`.
|
||||
'';
|
||||
example = "https://mcp.context7.com/mcp";
|
||||
};
|
||||
|
||||
headers = mkOption {
|
||||
type = lib.types.attrsOf lib.types.str;
|
||||
default = { };
|
||||
description = ''
|
||||
HTTP headers for requests to a remote MCP server.
|
||||
Only valid for remote servers.
|
||||
'';
|
||||
example = literalExpression ''
|
||||
{ MY_API_KEY = "{env:MY_API_KEY}"; }
|
||||
'';
|
||||
};
|
||||
|
||||
enabled = mkOption {
|
||||
type = lib.types.nullOr lib.types.bool;
|
||||
default = null;
|
||||
description = ''
|
||||
Whether this MCP server is enabled.
|
||||
|
||||
If this option and `disabled` are both set, they must satisfy
|
||||
`enabled == !disabled`.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
meta.maintainers = with lib.maintainers; [ delafthi ];
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
delafthi
|
||||
malik
|
||||
];
|
||||
|
||||
options.programs.mcp = {
|
||||
enable = mkEnableOption "mcp";
|
||||
|
||||
servers = mkOption {
|
||||
inherit (jsonFormat) type;
|
||||
type = lib.types.attrsOf serverModule;
|
||||
default = { };
|
||||
example = literalExpression ''
|
||||
{
|
||||
everything = {
|
||||
command = "npx";
|
||||
args = [
|
||||
"-y"
|
||||
"@modelcontextprotocol/server-everything"
|
||||
];
|
||||
args = [ "-y" "@modelcontextprotocol/server-everything" ];
|
||||
};
|
||||
context7 = {
|
||||
url = "https://mcp.context7.com/mcp";
|
||||
headers = {
|
||||
CONTEXT7_API_KEY = "{env:CONTEXT7_API_KEY}";
|
||||
};
|
||||
headers.CONTEXT7_API_KEY = "{env:CONTEXT7_API_KEY}";
|
||||
};
|
||||
codeberg = {
|
||||
command = "/path/to/forgejo-mcp";
|
||||
args = [ "transport" "stdio" "--url" "https://codeberg.org" ];
|
||||
env.FORGEJO_ACCESS_TOKEN.file = "/run/secrets/codeberg_token";
|
||||
};
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
MCP server configurations written to
|
||||
{file}`XDG_CONFIG_HOME/mcp/mcp.json`
|
||||
MCP server configurations written to {file}`$XDG_CONFIG_HOME/mcp/mcp.json`.
|
||||
|
||||
Each server is either a local (stdio) server via {option}`command`
|
||||
or a remote (HTTP/SSE) server via {option}`url`.
|
||||
|
||||
Besides declared options, arbitrary MCP fields are allowed.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
xdg.configFile = mkIf (cfg.servers != { }) (
|
||||
let
|
||||
mcp-config = {
|
||||
mcpServers = cfg.servers;
|
||||
};
|
||||
in
|
||||
{
|
||||
"mcp/mcp.json".source = jsonFormat.generate "mcp.json" mcp-config;
|
||||
}
|
||||
assertions = lib.concatLists (
|
||||
lib.mapAttrsToList (name: server: [
|
||||
{
|
||||
assertion = (server.command != null) != (server.url != null);
|
||||
message = ''
|
||||
programs.mcp.servers.${name}: exactly one of `command` or `url` must be set.
|
||||
'';
|
||||
}
|
||||
{
|
||||
assertion = server.url == null || (server.args == [ ] && server.env == { });
|
||||
message = ''
|
||||
programs.mcp.servers.${name}: `args` and `env` are only valid for local servers (`command`).
|
||||
'';
|
||||
}
|
||||
{
|
||||
assertion = server.headers == { } || server.url != null;
|
||||
message = ''
|
||||
programs.mcp.servers.${name}: `headers` is only valid for remote servers (`url`).
|
||||
'';
|
||||
}
|
||||
{
|
||||
assertion =
|
||||
!(server.enabled != null && server ? disabled && server.disabled != null)
|
||||
|| server.enabled != server.disabled;
|
||||
message = ''
|
||||
programs.mcp.servers.${name}: `enabled` and `disabled` are set to incompatible values.
|
||||
'';
|
||||
}
|
||||
]) cfg.servers
|
||||
);
|
||||
|
||||
xdg.configFile = mkIf (cfg.servers != { }) {
|
||||
"mcp/mcp.json".source = jsonFormat.generate "mcp.json" {
|
||||
mcpServers = lib.mapAttrs (
|
||||
_: server:
|
||||
lib.hm.mcp.transformMcpServer {
|
||||
inherit server;
|
||||
extraTransforms = [ lib.hm.mcp.addType ];
|
||||
exclude = [ "serverUrl" ];
|
||||
}
|
||||
) cfg.servers;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,32 +18,39 @@ let
|
||||
|
||||
jsonFormat = pkgs.formats.json { };
|
||||
|
||||
transformMcpServer = name: server: {
|
||||
inherit name;
|
||||
value = {
|
||||
enabled = !(server.disabled or false);
|
||||
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 server ? url then
|
||||
{
|
||||
type = "remote";
|
||||
inherit (server) url;
|
||||
}
|
||||
// (lib.optionalAttrs (server ? headers) { inherit (server) headers; })
|
||||
else if server ? command then
|
||||
{
|
||||
type = "local";
|
||||
command = [ server.command ] ++ (server.args or [ ]);
|
||||
}
|
||||
// (lib.optionalAttrs (server ? env) { environment = server.env; })
|
||||
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.listToAttrs (lib.mapAttrsToList transformMcpServer config.programs.mcp.servers)
|
||||
lib.mapAttrs (
|
||||
_: server:
|
||||
lib.hm.mcp.transformMcpServer {
|
||||
inherit server;
|
||||
extraTransforms = [ toOpencodeShape ];
|
||||
exclude = [
|
||||
"args"
|
||||
"env"
|
||||
];
|
||||
}
|
||||
) config.programs.mcp.servers
|
||||
else
|
||||
{ };
|
||||
|
||||
@@ -63,9 +70,6 @@ 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 = !isPathLikeContent cfg.skills || lib.pathIsDirectory cfg.skills;
|
||||
assertion = !lib.hm.strings.isPathLike 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 (isPathLikeContent cfg.skills) {
|
||||
"opencode/skills" = mkIf (lib.hm.strings.isPathLike cfg.skills) {
|
||||
source = cfg.skills;
|
||||
recursive = true;
|
||||
};
|
||||
@@ -558,14 +562,14 @@ in
|
||||
)
|
||||
// lib.mapAttrs' (
|
||||
name: content:
|
||||
if isPathLikeContent content && lib.pathIsDirectory content then
|
||||
if lib.hm.strings.isPathLike content && lib.pathIsDirectory content then
|
||||
lib.nameValuePair "opencode/skills/${name}" {
|
||||
source = content;
|
||||
recursive = true;
|
||||
}
|
||||
else
|
||||
lib.nameValuePair "opencode/skills/${name}/SKILL.md" (
|
||||
if isPathLikeContent content then { source = content; } else { text = content; }
|
||||
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) (
|
||||
|
||||
@@ -70,32 +70,16 @@ let
|
||||
|
||||
isPath = p: builtins.isPath p || lib.isStorePath p;
|
||||
|
||||
transformMcpServerForVscode =
|
||||
name: server:
|
||||
let
|
||||
# Remove the disabled field from the server config
|
||||
cleanServer = lib.filterAttrs (n: _v: n != "disabled") server;
|
||||
in
|
||||
{
|
||||
inherit name;
|
||||
value = {
|
||||
enabled = !(server.disabled or false);
|
||||
}
|
||||
// (
|
||||
if server ? url then
|
||||
{
|
||||
type = "http";
|
||||
}
|
||||
// cleanServer
|
||||
else if server ? command then
|
||||
{
|
||||
type = "stdio";
|
||||
}
|
||||
// cleanServer
|
||||
else
|
||||
{ }
|
||||
);
|
||||
transformMcpServerForVscode = name: server: {
|
||||
inherit name;
|
||||
value = lib.hm.mcp.transformMcpServer {
|
||||
inherit server;
|
||||
extraTransforms = [
|
||||
lib.hm.mcp.addType
|
||||
(lib.hm.mcp.wrapEnvFilesCommand { inherit pkgs name; })
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
profileType = types.submodule {
|
||||
options = {
|
||||
|
||||
@@ -31,19 +31,21 @@ let
|
||||
|
||||
transformedMcpServers = lib.optionalAttrs (cfg.enableMcpIntegration && config.programs.mcp.enable) (
|
||||
lib.mapAttrs (
|
||||
_name: server:
|
||||
# NOTE: Convert shared programs.mcp fields to Zed config keys:
|
||||
# - removeAttrs drops keys that Zed does not use directly
|
||||
# - "disabled" becomes inverse "enabled"
|
||||
# See: https://zed.dev/docs/ai/mcp
|
||||
(lib.removeAttrs server [ "disabled" ])
|
||||
// {
|
||||
enabled = !(server.disabled or false);
|
||||
name: server:
|
||||
# See: https://zed.dev/docs/ai/mcp & https://github.com/zed-industries/zed/discussions/53780
|
||||
lib.hm.mcp.transformMcpServer {
|
||||
inherit server;
|
||||
extraTransforms = [
|
||||
lib.hm.mcp.addType
|
||||
(lib.hm.mcp.wrapEnvFilesCommand { inherit pkgs name; })
|
||||
];
|
||||
}
|
||||
) config.programs.mcp.servers
|
||||
);
|
||||
|
||||
settingMcpServers = lib.attrByPath [ "context_servers" ] { } cfg.userSettings;
|
||||
settingMcpServers = lib.mapAttrs (_: lib.hm.mcp.addType) (
|
||||
lib.attrByPath [ "context_servers" ] { } cfg.userSettings
|
||||
);
|
||||
mergedMcpServers = transformedMcpServers // settingMcpServers;
|
||||
|
||||
mergedSettings =
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
lib,
|
||||
buildEnv,
|
||||
mesa,
|
||||
libglvnd,
|
||||
libvdpau-va-gl,
|
||||
intel-media-driver,
|
||||
nvidia-vaapi-driver,
|
||||
@@ -9,17 +10,24 @@
|
||||
system,
|
||||
nvidia_x11 ? linuxPackages.nvidia_x11,
|
||||
addNvidia ? false,
|
||||
egl-wayland,
|
||||
egl-wayland2,
|
||||
egl-gbm,
|
||||
}:
|
||||
|
||||
buildEnv {
|
||||
name = "non-nixos-gpu";
|
||||
paths = [
|
||||
mesa
|
||||
libglvnd
|
||||
libvdpau-va-gl
|
||||
]
|
||||
++ lib.optional (system == "x86_64-linux") intel-media-driver
|
||||
++ lib.optionals addNvidia [
|
||||
nvidia_x11
|
||||
nvidia-vaapi-driver
|
||||
egl-wayland
|
||||
egl-wayland2
|
||||
egl-gbm
|
||||
];
|
||||
}
|
||||
|
||||
@@ -27,5 +27,15 @@ stdenv.mkDerivation {
|
||||
mkdir -p $out/{bin,resources,lib/tmpfiles.d}
|
||||
cp non-nixos-gpu-setup $out/bin
|
||||
cp non-nixos-gpu.conf $out/lib/tmpfiles.d
|
||||
|
||||
# Add Nvidia EGL config, when present
|
||||
if [[ -d "${nonNixosGpuEnv}/share/egl/egl_external_platform.d" ]]; then
|
||||
for path in "${nonNixosGpuEnv}/share/egl/egl_external_platform.d"/*; do
|
||||
fname=$(basename "$path")
|
||||
dstname=''${fname%.json}_nix_gpu.json
|
||||
echo "L+ /etc/egl/egl_external_platform.d/$dstname - - - - $path" \
|
||||
>> $out/lib/tmpfiles.d/non-nixos-gpu.conf
|
||||
done
|
||||
fi
|
||||
'';
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"release": "26.05",
|
||||
"isReleaseBranch": false
|
||||
"isReleaseBranch": true
|
||||
}
|
||||
|
||||
@@ -148,6 +148,7 @@ let
|
||||
"openssh"
|
||||
"openstackclient"
|
||||
"papis"
|
||||
"parallel-full"
|
||||
"patdiff"
|
||||
"pay-respects"
|
||||
"pet"
|
||||
|
||||
@@ -178,6 +178,7 @@ import nmtSrc {
|
||||
[
|
||||
# keep-sorted start case=no numeric=yes
|
||||
./lib/generators
|
||||
./lib/mcp
|
||||
./lib/types
|
||||
./modules/files
|
||||
./modules/home-environment
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
lib-mcp-wrapper = ./wrapper.nix;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{ pkgs, lib, ... }:
|
||||
let
|
||||
fakeSecret = pkgs.writeText "fake-npm-token" "supersecret";
|
||||
|
||||
wrapper = lib.hm.mcp.mkEnvFilesWrapper {
|
||||
inherit pkgs;
|
||||
name = "test-server";
|
||||
server = {
|
||||
command = "npx";
|
||||
args = [
|
||||
"-y"
|
||||
"@modelcontextprotocol/server-everything"
|
||||
];
|
||||
env.NPM_TOKEN.file = "${fakeSecret}";
|
||||
};
|
||||
};
|
||||
|
||||
wrapperNoArgs = lib.hm.mcp.mkEnvFilesWrapper {
|
||||
inherit pkgs;
|
||||
name = "test-server-noargs";
|
||||
server = {
|
||||
command = "npx";
|
||||
env.NPM_TOKEN.file = "${fakeSecret}";
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
home.file."mcp-wrapper-under-test" = {
|
||||
source = wrapper;
|
||||
};
|
||||
home.file."mcp-wrapper-noargs-under-test" = {
|
||||
source = wrapperNoArgs;
|
||||
};
|
||||
|
||||
nmt.script = ''
|
||||
assertFileContains home-files/mcp-wrapper-under-test \
|
||||
'if NPM_TOKEN=$(cat ${fakeSecret}); then'
|
||||
assertFileContains home-files/mcp-wrapper-under-test \
|
||||
'export NPM_TOKEN'
|
||||
assertFileContains home-files/mcp-wrapper-under-test \
|
||||
'exec npx -y @modelcontextprotocol/server-everything'
|
||||
assertFileContains home-files/mcp-wrapper-noargs-under-test \
|
||||
'exec npx'
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
options,
|
||||
...
|
||||
}:
|
||||
|
||||
{
|
||||
programs.gemini-cli = {
|
||||
enable = true;
|
||||
package = pkgs.writeShellScriptBin "gemini-cli" "";
|
||||
context = {
|
||||
# Test inline content
|
||||
GEMINI = ''
|
||||
# Global Context
|
||||
|
||||
You are a helpful AI assistant for software development.
|
||||
|
||||
## Coding Standards
|
||||
|
||||
- Follow consistent code style
|
||||
- Write clear comments
|
||||
- Test your changes
|
||||
'';
|
||||
# Test file path
|
||||
AGENTS = ./context.md;
|
||||
# Test another inline content
|
||||
CONTEXT = ''
|
||||
Additional context for specialized tasks.
|
||||
'';
|
||||
};
|
||||
settings = {
|
||||
context.fileName = [
|
||||
"AGENTS.md"
|
||||
"CONTEXT.md"
|
||||
"GEMINI.md"
|
||||
];
|
||||
};
|
||||
};
|
||||
test.asserts.warnings.expected = [
|
||||
"The option `programs.gemini-cli' defined in ${lib.showFiles options.programs.gemini-cli.files} has been renamed to `programs.antigravity-cli'."
|
||||
];
|
||||
|
||||
nmt.script = ''
|
||||
assertFileExists home-files/.gemini/GEMINI.md
|
||||
assertFileContent home-files/.gemini/GEMINI.md \
|
||||
${./context-inline.md}
|
||||
|
||||
assertFileExists home-files/.gemini/AGENTS.md
|
||||
assertFileContent home-files/.gemini/AGENTS.md \
|
||||
${./context.md}
|
||||
|
||||
assertFileExists home-files/.gemini/CONTEXT.md
|
||||
assertFileContent home-files/.gemini/CONTEXT.md \
|
||||
${./context-additional.md}
|
||||
|
||||
assertFileExists home-path/etc/profile.d/hm-session-vars.sh
|
||||
assertFileNotRegex home-path/etc/profile.d/hm-session-vars.sh \
|
||||
"GEMINI_MODEL"
|
||||
'';
|
||||
}
|
||||
+1
@@ -6,4 +6,5 @@
|
||||
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;
|
||||
gemini-cli-policies = ./policies.nix;
|
||||
}
|
||||
+17
@@ -1,7 +1,15 @@
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
options,
|
||||
...
|
||||
}:
|
||||
|
||||
{
|
||||
programs = {
|
||||
gemini-cli = {
|
||||
enable = true;
|
||||
package = pkgs.writeShellScriptBin "gemini-cli" "";
|
||||
enableMcpIntegration = true;
|
||||
settings = {
|
||||
theme = "Default";
|
||||
@@ -9,6 +17,9 @@
|
||||
mcpServers = {
|
||||
github = {
|
||||
url = "https://api.githubcopilot.com/mcp/";
|
||||
env = {
|
||||
url = "https://token.example/env";
|
||||
};
|
||||
};
|
||||
filesystem = {
|
||||
command = "npx";
|
||||
@@ -47,9 +58,15 @@
|
||||
};
|
||||
};
|
||||
};
|
||||
test.asserts.warnings.expected = [
|
||||
"The option `programs.gemini-cli' defined in ${lib.showFiles options.programs.gemini-cli.files} has been renamed to `programs.antigravity-cli'."
|
||||
];
|
||||
|
||||
nmt.script = ''
|
||||
assertFileExists home-files/.gemini/settings.json
|
||||
assertFileRegex home-files/.gemini/settings.json '"github"'
|
||||
assertFileRegex home-files/.gemini/settings.json '"url"'
|
||||
assertFileRegex home-files/.gemini/settings.json 'https://token.example/env'
|
||||
assertFileRegex home-files/.gemini/settings.json '"filesystem"'
|
||||
assertFileRegex home-files/.gemini/settings.json '"database"'
|
||||
assertFileNotRegex home-files/.gemini/settings.json '"other-tmp"'
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
[[rule]]
|
||||
commandPrefix = "git "
|
||||
toolName = "run_shell_command"
|
||||
commandPrefix = "nix "
|
||||
decision = "ask_user"
|
||||
priority = 100
|
||||
toolName = "run_shell_command"
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
options,
|
||||
...
|
||||
}:
|
||||
|
||||
{
|
||||
programs.gemini-cli = {
|
||||
enable = true;
|
||||
package = pkgs.writeShellScriptBin "gemini-cli" "";
|
||||
policies = {
|
||||
"my-rules" = {
|
||||
rule = [
|
||||
{
|
||||
toolName = "run_shell_command";
|
||||
commandPrefix = "git ";
|
||||
decision = "ask_user";
|
||||
priority = 100;
|
||||
}
|
||||
];
|
||||
};
|
||||
"other-rules" = ./other-rules.toml;
|
||||
};
|
||||
};
|
||||
|
||||
test.asserts.warnings.expected = [
|
||||
"The option `programs.gemini-cli' defined in ${lib.showFiles options.programs.gemini-cli.files} has been renamed to `programs.antigravity-cli'."
|
||||
];
|
||||
|
||||
nmt.script = ''
|
||||
assertFileExists home-files/.gemini/policies/my-rules.toml
|
||||
assertFileRegex home-files/.gemini/policies/my-rules.toml \
|
||||
'toolName = "run_shell_command"'
|
||||
assertFileRegex home-files/.gemini/policies/my-rules.toml \
|
||||
'commandPrefix = "git "'
|
||||
|
||||
assertFileExists home-files/.gemini/policies/other-rules.toml
|
||||
assertFileRegex home-files/.gemini/policies/other-rules.toml \
|
||||
'toolName = "run_shell_command"'
|
||||
assertFileRegex home-files/.gemini/policies/other-rules.toml \
|
||||
'commandPrefix = "nix "'
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"autoAccept": true,
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"command(git)"
|
||||
],
|
||||
"ask": [
|
||||
"command(*)"
|
||||
],
|
||||
"deny": [
|
||||
"command(rm -rf)"
|
||||
]
|
||||
},
|
||||
"preferredEditor": "nvim",
|
||||
"theme": "Default",
|
||||
"vimMode": true
|
||||
}
|
||||
+26
-20
@@ -1,6 +1,14 @@
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
options,
|
||||
...
|
||||
}:
|
||||
|
||||
{
|
||||
programs.gemini-cli = {
|
||||
enable = true;
|
||||
package = pkgs.writeShellScriptBin "gemini-cli" "";
|
||||
defaultModel = "gemini-2.5-flash";
|
||||
settings = {
|
||||
theme = "Default";
|
||||
@@ -20,32 +28,30 @@
|
||||
description = "Generates a fix for a given GitHub issue.";
|
||||
};
|
||||
};
|
||||
policies = {
|
||||
"my-rules" = {
|
||||
rule = [
|
||||
{
|
||||
toolName = "run_shell_command";
|
||||
commandPrefix = "git ";
|
||||
decision = "ask_user";
|
||||
priority = 100;
|
||||
}
|
||||
];
|
||||
};
|
||||
"other-rules" = ./other-rules.toml;
|
||||
permissions = {
|
||||
allow = [ "command(git)" ];
|
||||
deny = [ "command(rm -rf)" ];
|
||||
ask = [ "command(*)" ];
|
||||
};
|
||||
};
|
||||
test.asserts.warnings.expected = [
|
||||
"The option `programs.gemini-cli' defined in ${lib.showFiles options.programs.gemini-cli.files} has been renamed to `programs.antigravity-cli'."
|
||||
];
|
||||
|
||||
nmt.script = ''
|
||||
assertFileExists home-files/.gemini/settings.json
|
||||
assertFileContent home-files/.gemini/settings.json \
|
||||
${./settings.json}
|
||||
assertFileContent home-files/.gemini/commands/changelog.toml \
|
||||
${./changelog.toml}
|
||||
assertFileContent home-files/.gemini/commands/git/fix.toml \
|
||||
${./fix.toml}
|
||||
assertFileContent home-files/.gemini/policies/my-rules.toml \
|
||||
${./my-rules.toml}
|
||||
assertFileContent home-files/.gemini/policies/other-rules.toml \
|
||||
${./other-rules.toml}
|
||||
assertFileExists home-files/.gemini/commands/changelog.toml
|
||||
assertFileRegex home-files/.gemini/commands/changelog.toml \
|
||||
'prompt ='
|
||||
assertFileRegex home-files/.gemini/commands/changelog.toml \
|
||||
"Adds a new entry to the project's CHANGELOG.md file."
|
||||
assertFileExists home-files/.gemini/commands/git/fix.toml
|
||||
assertFileRegex home-files/.gemini/commands/git/fix.toml \
|
||||
'prompt ='
|
||||
assertFileRegex home-files/.gemini/commands/git/fix.toml \
|
||||
'Generates a fix for a given GitHub issue.'
|
||||
|
||||
assertFileExists home-path/etc/profile.d/hm-session-vars.sh
|
||||
assertFileContains home-path/etc/profile.d/hm-session-vars.sh \
|
||||
+12
@@ -1,9 +1,21 @@
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
options,
|
||||
...
|
||||
}:
|
||||
|
||||
{
|
||||
programs.gemini-cli = {
|
||||
enable = true;
|
||||
package = pkgs.writeShellScriptBin "gemini-cli" "";
|
||||
skills = ./skills;
|
||||
};
|
||||
|
||||
test.asserts.warnings.expected = [
|
||||
"The option `programs.gemini-cli' defined in ${lib.showFiles options.programs.gemini-cli.files} has been renamed to `programs.antigravity-cli'."
|
||||
];
|
||||
|
||||
nmt.script = ''
|
||||
assertFileExists home-files/.gemini/skills/xlsx/SKILL.md
|
||||
assertLinkExists home-files/.gemini/skills/xlsx/SKILL.md
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
options,
|
||||
...
|
||||
}:
|
||||
|
||||
{
|
||||
programs.gemini-cli = {
|
||||
enable = true;
|
||||
package = pkgs.writeShellScriptBin "gemini-cli" "";
|
||||
skills = ./skills/xlsx/SKILL.md;
|
||||
};
|
||||
|
||||
test.asserts.warnings.expected = [
|
||||
"The option `programs.gemini-cli' defined in ${lib.showFiles options.programs.gemini-cli.files} has been renamed to `programs.antigravity-cli'."
|
||||
];
|
||||
|
||||
test.asserts.assertions.expected = [
|
||||
"`programs.antigravity-cli.skills` must be a directory when set to a path"
|
||||
];
|
||||
}
|
||||
+11
-1
@@ -1,4 +1,9 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
options,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
src = pkgs.writeTextDir "skills/external-skill/SKILL.md" ''
|
||||
@@ -8,9 +13,14 @@ in
|
||||
{
|
||||
programs.gemini-cli = {
|
||||
enable = true;
|
||||
package = pkgs.writeShellScriptBin "gemini-cli" "";
|
||||
skills = "${src}/skills";
|
||||
};
|
||||
|
||||
test.asserts.warnings.expected = [
|
||||
"The option `programs.gemini-cli' defined in ${lib.showFiles options.programs.gemini-cli.files} has been renamed to `programs.antigravity-cli'."
|
||||
];
|
||||
|
||||
nmt.script = ''
|
||||
assertFileExists home-files/.gemini/skills/external-skill/SKILL.md
|
||||
assertLinkExists home-files/.gemini/skills/external-skill/SKILL.md
|
||||
+12
@@ -1,6 +1,14 @@
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
options,
|
||||
...
|
||||
}:
|
||||
|
||||
{
|
||||
programs.gemini-cli = {
|
||||
enable = true;
|
||||
package = pkgs.writeShellScriptBin "gemini-cli" "";
|
||||
skills = {
|
||||
xlsx = ./skills/xlsx/SKILL.md;
|
||||
data-analysis = ./skills/data-analysis;
|
||||
@@ -25,6 +33,10 @@
|
||||
'';
|
||||
};
|
||||
};
|
||||
test.asserts.warnings.expected = [
|
||||
"The option `programs.gemini-cli' defined in ${lib.showFiles options.programs.gemini-cli.files} has been renamed to `programs.antigravity-cli'."
|
||||
];
|
||||
|
||||
nmt.script = ''
|
||||
assertFileExists home-files/.gemini/skills/xlsx/SKILL.md
|
||||
assertFileContent home-files/.gemini/skills/xlsx/SKILL.md \
|
||||
@@ -0,0 +1 @@
|
||||
Additional context for specialized tasks.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Global Context
|
||||
|
||||
You are a helpful AI assistant for software development.
|
||||
|
||||
## Coding Standards
|
||||
|
||||
- Follow consistent code style
|
||||
- Write clear comments
|
||||
- Test your changes
|
||||
@@ -0,0 +1,9 @@
|
||||
# Global Context
|
||||
|
||||
You are a helpful AI assistant for software development.
|
||||
|
||||
## Coding Standards
|
||||
|
||||
- Follow consistent code style
|
||||
- Write clear comments
|
||||
- Test your changes
|
||||
+4
-1
@@ -1,6 +1,9 @@
|
||||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
programs.gemini-cli = {
|
||||
programs.antigravity-cli = {
|
||||
enable = true;
|
||||
package = pkgs.writeShellScriptBin "antigravity-cli" "";
|
||||
context = {
|
||||
# Test inline content
|
||||
GEMINI = ''
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
antigravity-cli-settings = ./settings.nix;
|
||||
antigravity-cli-context = ./context.nix;
|
||||
antigravity-cli-skills = ./skills.nix;
|
||||
antigravity-cli-skills-dir = ./skills-dir.nix;
|
||||
antigravity-cli-skills-store-path-dir = ./skills-store-path-dir.nix;
|
||||
antigravity-cli-skills-path-not-directory = ./skills-path-not-directory.nix;
|
||||
antigravity-cli-mcp-servers = ./mcp.nix;
|
||||
antigravity-cli-policies-removed = ./policies-removed.nix;
|
||||
antigravity-cli-gemini-package-paths = ./gemini-package-paths.nix;
|
||||
antigravity-cli-legacy-gemini-config = ./legacy-gemini-config.nix;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
programs.antigravity-cli = {
|
||||
enable = true;
|
||||
package = pkgs.writeShellScriptBin "gemini-cli" "";
|
||||
settings.theme = "Default";
|
||||
commands.review = {
|
||||
prompt = "Review current changes.";
|
||||
description = "Reviews current changes.";
|
||||
};
|
||||
skills.audit = ''
|
||||
---
|
||||
name: audit
|
||||
description: Audit code changes.
|
||||
---
|
||||
|
||||
Audit code changes.
|
||||
'';
|
||||
policies.commands.rule = [
|
||||
{
|
||||
toolName = "run_shell_command";
|
||||
commandPrefix = "git ";
|
||||
decision = "ask_user";
|
||||
priority = 100;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
nmt.script = ''
|
||||
assertFileExists home-files/.gemini/settings.json
|
||||
assertPathNotExists home-files/.gemini/antigravity-cli/settings.json
|
||||
assertFileExists home-files/.gemini/commands/review.toml
|
||||
assertFileExists home-files/.gemini/skills/audit/SKILL.md
|
||||
assertFileExists home-files/.gemini/policies/commands.toml
|
||||
assertPathNotExists home-files/.gemini/config/skills/review/SKILL.md
|
||||
assertPathNotExists home-files/.gemini/config/mcp_config.json
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
programs.antigravity-cli = {
|
||||
enable = true;
|
||||
package = pkgs.writeShellScriptBin "antigravity-cli" "";
|
||||
useLegacyGeminiConfig = true;
|
||||
settings.theme = "Default";
|
||||
mcpServers.github.serverUrl = "https://api.githubcopilot.com/mcp/";
|
||||
commands.review = {
|
||||
prompt = "Review current changes.";
|
||||
description = "Reviews current changes.";
|
||||
};
|
||||
};
|
||||
|
||||
nmt.script = ''
|
||||
assertFileExists home-files/.gemini/settings.json
|
||||
assertFileExists home-files/.gemini/commands/review.toml
|
||||
assertFileRegex home-files/.gemini/settings.json '"github"'
|
||||
assertPathNotExists home-files/.gemini/antigravity-cli/settings.json
|
||||
assertPathNotExists home-files/.gemini/config/mcp_config.json
|
||||
assertPathNotExists home-files/.gemini/config/skills/review/SKILL.md
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
programs = {
|
||||
antigravity-cli = {
|
||||
enable = true;
|
||||
package = pkgs.writeShellScriptBin "antigravity-cli" "";
|
||||
enableMcpIntegration = true;
|
||||
settings = {
|
||||
colorScheme = "terminal";
|
||||
toolPermission = "request-review";
|
||||
};
|
||||
mcpServers = {
|
||||
github = {
|
||||
url = "https://api.githubcopilot.com/mcp/";
|
||||
env = {
|
||||
url = "https://token.example/env";
|
||||
};
|
||||
};
|
||||
filesystem = {
|
||||
command = "npx";
|
||||
args = [
|
||||
"-y"
|
||||
"@modelcontextprotocol/server-filesystem"
|
||||
"/tmp"
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
mcp = {
|
||||
enable = true;
|
||||
servers = {
|
||||
filesystem = {
|
||||
command = "npx";
|
||||
args = [
|
||||
"-y"
|
||||
"@modelcontextprotocol/server-filesystem"
|
||||
"/other-tmp"
|
||||
];
|
||||
};
|
||||
database = {
|
||||
command = "npx";
|
||||
args = [
|
||||
"-y"
|
||||
"@bytebase/dbhub"
|
||||
"--dsn"
|
||||
"postgresql://user:pass@localhost:5432/db"
|
||||
];
|
||||
env = {
|
||||
DATABASE_URL = "postgresql://user:pass@localhost:5432/db";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
nmt.script = ''
|
||||
assertFileExists home-files/.gemini/config/mcp_config.json
|
||||
assertFileRegex home-files/.gemini/config/mcp_config.json '"github"'
|
||||
assertFileRegex home-files/.gemini/config/mcp_config.json '"serverUrl"'
|
||||
assertFileRegex home-files/.gemini/config/mcp_config.json '"url"'
|
||||
assertFileRegex home-files/.gemini/config/mcp_config.json 'https://token.example/env'
|
||||
assertFileRegex home-files/.gemini/config/mcp_config.json '"filesystem"'
|
||||
assertFileRegex home-files/.gemini/config/mcp_config.json '"database"'
|
||||
assertFileNotRegex home-files/.gemini/config/mcp_config.json '"other-tmp"'
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
_:
|
||||
|
||||
{
|
||||
programs.antigravity-cli = {
|
||||
enable = true;
|
||||
package = null;
|
||||
policies."my-rules" = {
|
||||
rule = [
|
||||
{
|
||||
toolName = "run_shell_command";
|
||||
commandPrefix = "git ";
|
||||
decision = "ask_user";
|
||||
priority = 100;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
test.asserts.assertions.expected = [
|
||||
''
|
||||
`programs.antigravity-cli.policies` is only supported when
|
||||
`programs.antigravity-cli.package` is a `gemini-cli` package.
|
||||
Antigravity CLI configures permissions in
|
||||
`programs.antigravity-cli.settings.permissions` or
|
||||
`programs.antigravity-cli.permissions`.
|
||||
''
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"altScreenMode": "always",
|
||||
"artifactReviewPolicy": "agent-decides",
|
||||
"colorScheme": "tokyo night",
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"command(git)"
|
||||
],
|
||||
"ask": [
|
||||
"command(*)"
|
||||
],
|
||||
"deny": [
|
||||
"command(rm -rf)"
|
||||
]
|
||||
},
|
||||
"toolPermission": "proceed-in-sandbox"
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
programs.antigravity-cli = {
|
||||
enable = true;
|
||||
package = pkgs.writeShellScriptBin "antigravity-cli" "";
|
||||
defaultModel = "gemini-2.5-flash";
|
||||
settings = {
|
||||
colorScheme = "tokyo night";
|
||||
altScreenMode = "always";
|
||||
toolPermission = "proceed-in-sandbox";
|
||||
artifactReviewPolicy = "agent-decides";
|
||||
};
|
||||
commands = {
|
||||
changelog = {
|
||||
prompt = ''
|
||||
Your task is to parse the `<version>`, `<change_type>`, and `<message>` from their input and use the `write_file` tool to correctly update the `CHANGELOG.md` file.
|
||||
'';
|
||||
description = "Adds a new entry to the project's CHANGELOG.md file.";
|
||||
};
|
||||
"git/fix" = {
|
||||
prompt = "Please analyze the staged git changes and provide a code fix for the issue described here: {{args}}.";
|
||||
description = "Generates a fix for a given GitHub issue.";
|
||||
};
|
||||
};
|
||||
permissions = {
|
||||
allow = [ "command(git)" ];
|
||||
deny = [ "command(rm -rf)" ];
|
||||
ask = [ "command(*)" ];
|
||||
};
|
||||
};
|
||||
nmt.script = ''
|
||||
assertFileExists home-files/.gemini/antigravity-cli/settings.json
|
||||
assertFileContent home-files/.gemini/antigravity-cli/settings.json \
|
||||
${./settings.json}
|
||||
assertFileExists home-files/.gemini/config/skills/changelog/SKILL.md
|
||||
assertFileRegex home-files/.gemini/config/skills/changelog/SKILL.md \
|
||||
'name: changelog'
|
||||
assertFileRegex home-files/.gemini/config/skills/changelog/SKILL.md \
|
||||
"Adds a new entry to the project's CHANGELOG.md file."
|
||||
assertFileExists home-files/.gemini/config/skills/git:fix/SKILL.md
|
||||
assertFileRegex home-files/.gemini/config/skills/git:fix/SKILL.md \
|
||||
'name: git:fix'
|
||||
assertFileRegex home-files/.gemini/config/skills/git:fix/SKILL.md \
|
||||
'Generates a fix for a given GitHub issue.'
|
||||
|
||||
assertFileExists home-path/etc/profile.d/hm-session-vars.sh
|
||||
assertFileContains home-path/etc/profile.d/hm-session-vars.sh \
|
||||
'export GEMINI_MODEL="gemini-2.5-flash"'
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
programs.antigravity-cli = {
|
||||
enable = true;
|
||||
package = pkgs.writeShellScriptBin "antigravity-cli" "";
|
||||
skills = ./skills;
|
||||
};
|
||||
|
||||
nmt.script = ''
|
||||
assertFileExists home-files/.gemini/config/skills/xlsx/SKILL.md
|
||||
assertLinkExists home-files/.gemini/config/skills/xlsx/SKILL.md
|
||||
assertFileContent home-files/.gemini/config/skills/xlsx/SKILL.md \
|
||||
${./skills/xlsx/SKILL.md}
|
||||
|
||||
assertFileExists home-files/.gemini/config/skills/data-analysis/SKILL.md
|
||||
assertLinkExists home-files/.gemini/config/skills/data-analysis/SKILL.md
|
||||
assertFileContent home-files/.gemini/config/skills/data-analysis/SKILL.md \
|
||||
${./skills/data-analysis/SKILL.md}
|
||||
|
||||
assertFileExists home-files/.gemini/config/skills/pdf-processing/SKILL.md
|
||||
assertLinkExists home-files/.gemini/config/skills/pdf-processing/SKILL.md
|
||||
assertFileContent home-files/.gemini/config/skills/pdf-processing/SKILL.md \
|
||||
${./skills/pdf-processing/SKILL.md}
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
programs.antigravity-cli = {
|
||||
enable = true;
|
||||
package = pkgs.writeShellScriptBin "antigravity-cli" "";
|
||||
skills = ./skills/xlsx/SKILL.md;
|
||||
};
|
||||
|
||||
test.asserts.assertions.expected = [
|
||||
"`programs.antigravity-cli.skills` must be a directory when set to a path"
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{ pkgs, ... }:
|
||||
|
||||
let
|
||||
src = pkgs.writeTextDir "skills/external-skill/SKILL.md" ''
|
||||
# External Skill
|
||||
'';
|
||||
in
|
||||
{
|
||||
programs.antigravity-cli = {
|
||||
enable = true;
|
||||
package = pkgs.writeShellScriptBin "antigravity-cli" "";
|
||||
skills = "${src}/skills";
|
||||
};
|
||||
|
||||
nmt.script = ''
|
||||
assertFileExists home-files/.gemini/config/skills/external-skill/SKILL.md
|
||||
assertLinkExists home-files/.gemini/config/skills/external-skill/SKILL.md
|
||||
assertFileContent home-files/.gemini/config/skills/external-skill/SKILL.md \
|
||||
"${src}/skills/external-skill/SKILL.md"
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
programs.antigravity-cli = {
|
||||
enable = true;
|
||||
package = pkgs.writeShellScriptBin "antigravity-cli" "";
|
||||
skills = {
|
||||
xlsx = ./skills/xlsx/SKILL.md;
|
||||
data-analysis = ./skills/data-analysis;
|
||||
pdf-processing = ''
|
||||
---
|
||||
name: pdf-processing
|
||||
description: Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDF files or when the user mentions PDFs, forms, or document extraction.
|
||||
---
|
||||
|
||||
# PDF Processing
|
||||
|
||||
## Quick start
|
||||
|
||||
Use pdfplumber to extract text from PDFs:
|
||||
|
||||
```python
|
||||
import pdfplumber
|
||||
|
||||
with pdfplumber.open("document.pdf") as pdf:
|
||||
text = pdf.pages[0].extract_text()
|
||||
```
|
||||
'';
|
||||
};
|
||||
};
|
||||
nmt.script = ''
|
||||
assertFileExists home-files/.gemini/config/skills/xlsx/SKILL.md
|
||||
assertFileContent home-files/.gemini/config/skills/xlsx/SKILL.md \
|
||||
${./skills/xlsx/SKILL.md}
|
||||
|
||||
assertFileExists home-files/.gemini/config/skills/data-analysis/SKILL.md
|
||||
assertFileContent home-files/.gemini/config/skills/data-analysis/SKILL.md \
|
||||
${./skills/data-analysis/SKILL.md}
|
||||
|
||||
assertFileExists home-files/.gemini/config/skills/pdf-processing/SKILL.md
|
||||
assertFileContent home-files/.gemini/config/skills/pdf-processing/SKILL.md \
|
||||
${./skills/pdf-processing/SKILL.md}
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
name: data-analysis
|
||||
description: Do some data analysis
|
||||
---
|
||||
|
||||
# Data Analysis
|
||||
|
||||
Use good data analytics tools for analysis 👍
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: pdf-processing
|
||||
description: Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDF files or when the user mentions PDFs, forms, or document extraction.
|
||||
---
|
||||
|
||||
# PDF Processing
|
||||
|
||||
## Quick start
|
||||
|
||||
Use pdfplumber to extract text from PDFs:
|
||||
|
||||
```python
|
||||
import pdfplumber
|
||||
|
||||
with pdfplumber.open("document.pdf") as pdf:
|
||||
text = pdf.pages[0].extract_text()
|
||||
```
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
name: xlsx
|
||||
description: Handling xlsx files correctly
|
||||
---
|
||||
|
||||
# XLSX Handling
|
||||
|
||||
Use the python library called `openpyxl` to handle these files
|
||||
@@ -66,6 +66,7 @@
|
||||
assertFileExists home-files/.config/claude/commands/hello.md
|
||||
assertFileExists home-files/.config/claude/rules/style.md
|
||||
assertFileExists home-files/.config/claude/hooks/pre-edit
|
||||
assertFileIsExecutable home-files/.config/claude/hooks/pre-edit
|
||||
assertFileExists home-files/.config/claude/output-styles/concise.md
|
||||
assertFileExists home-files/.config/claude/skills/pdf/SKILL.md
|
||||
|
||||
|
||||
@@ -135,9 +135,11 @@
|
||||
assertFileContent home-files/.claude/commands/commit.md ${./expected-commit}
|
||||
|
||||
assertFileExists home-files/.claude/hooks/pre-edit
|
||||
assertFileIsExecutable home-files/.claude/hooks/pre-edit
|
||||
assertFileRegex home-files/.claude/hooks/pre-edit "About to edit file"
|
||||
|
||||
assertFileExists home-files/.claude/hooks/post-commit
|
||||
assertFileIsExecutable home-files/.claude/hooks/post-commit
|
||||
assertFileRegex home-files/.claude/hooks/post-commit "Committed with message"
|
||||
'';
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
model = "gpt-5-codex"
|
||||
|
||||
[mcp_servers.context7]
|
||||
enabled = true
|
||||
url = "https://mcp.context7.com/mcp"
|
||||
|
||||
[mcp_servers.context7.http_headers]
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
[mcp_servers.context7]
|
||||
enabled = true
|
||||
url = "https://mcp.context7.com/mcp"
|
||||
|
||||
[mcp_servers.context7.http_headers]
|
||||
@@ -13,4 +12,3 @@ enabled = false
|
||||
[mcp_servers.everything]
|
||||
args = ["-y", "@modelcontextprotocol/server-everything"]
|
||||
command = "npx"
|
||||
enabled = true
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
description = "Adds a new entry to the project's CHANGELOG.md file."
|
||||
prompt = "Your task is to parse the `<version>`, `<change_type>`, and `<message>` from their input and use the `write_file` tool to correctly update the `CHANGELOG.md` file.\n"
|
||||
@@ -1,2 +0,0 @@
|
||||
description = "Generates a fix for a given GitHub issue."
|
||||
prompt = "Please analyze the staged git changes and provide a code fix for the issue described here: {{args}}."
|
||||
@@ -1,4 +0,0 @@
|
||||
[[rule]]
|
||||
toolName = "read_file"
|
||||
decision = "allow"
|
||||
priority = 50
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"autoAccept": true,
|
||||
"preferredEditor": "nvim",
|
||||
"theme": "Default",
|
||||
"vimMode": true
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
programs.gemini-cli = {
|
||||
enable = true;
|
||||
skills = ./skills/xlsx/SKILL.md;
|
||||
};
|
||||
|
||||
test.asserts.assertions.expected = [
|
||||
"`programs.gemini-cli.skills` must be a directory when set to a path"
|
||||
];
|
||||
}
|
||||
@@ -15,7 +15,6 @@
|
||||
"type": "local"
|
||||
},
|
||||
"fetch": {
|
||||
"args": [],
|
||||
"command": "mcp-server-fetch",
|
||||
"tools": [
|
||||
"*"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
mcp-servers = ./servers.nix;
|
||||
mcp-empty-servers = ./empty-servers.nix;
|
||||
mcp-enabled-disabled-conflict = ./enabled-disabled-conflict.nix;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
programs.mcp = {
|
||||
enable = true;
|
||||
servers = {
|
||||
invalid = {
|
||||
command = "echo";
|
||||
args = [ "test" ];
|
||||
enabled = true;
|
||||
disabled = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
test.asserts.assertions.expected = [
|
||||
''
|
||||
programs.mcp.servers.invalid: `enabled` and `disabled` are set to incompatible values.
|
||||
''
|
||||
];
|
||||
}
|
||||
@@ -4,14 +4,19 @@
|
||||
"headers": {
|
||||
"CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}"
|
||||
},
|
||||
"serverUrl": "https://mcp.context7.com/mcp"
|
||||
"type": "http",
|
||||
"url": "https://mcp.context7.com/mcp"
|
||||
},
|
||||
"everything": {
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-everything"
|
||||
],
|
||||
"command": "npx"
|
||||
"command": "npx",
|
||||
"env": {
|
||||
"NPM_TOKEN": "{file:/run/secrets/npm-token}"
|
||||
},
|
||||
"type": "stdio"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
"-y"
|
||||
"@modelcontextprotocol/server-everything"
|
||||
];
|
||||
env.NPM_TOKEN.file = "/run/secrets/npm-token";
|
||||
};
|
||||
context7 = {
|
||||
serverUrl = "https://mcp.context7.com/mcp";
|
||||
url = "https://mcp.context7.com/mcp";
|
||||
headers = {
|
||||
CONTEXT7_API_KEY = "{env:CONTEXT7_API_KEY}";
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"context7": {
|
||||
"enabled": true,
|
||||
"headers": {
|
||||
"CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}"
|
||||
},
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"context7": {
|
||||
"enabled": true,
|
||||
"headers": {
|
||||
"CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}"
|
||||
},
|
||||
@@ -23,7 +22,9 @@
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-everything"
|
||||
],
|
||||
"enabled": true,
|
||||
"environment": {
|
||||
"NPM_TOKEN": "{file:/run/secrets/npm-token}"
|
||||
},
|
||||
"type": "local"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"-y"
|
||||
"@modelcontextprotocol/server-everything"
|
||||
];
|
||||
env.NPM_TOKEN.file = "/run/secrets/npm-token";
|
||||
};
|
||||
context7 = {
|
||||
url = "https://mcp.context7.com/mcp";
|
||||
@@ -18,7 +19,7 @@
|
||||
disabled-server = {
|
||||
command = "echo";
|
||||
args = [ "test" ];
|
||||
disabled = true;
|
||||
enabled = false;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"servers": {
|
||||
"context7": {
|
||||
"enabled": true,
|
||||
"headers": {
|
||||
"CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}"
|
||||
},
|
||||
@@ -19,7 +18,6 @@
|
||||
"@modelcontextprotocol/server-everything"
|
||||
],
|
||||
"command": "npx",
|
||||
"enabled": true,
|
||||
"type": "stdio"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"url": "https://example.com/mcp"
|
||||
},
|
||||
"context7": {
|
||||
"enabled": true,
|
||||
"headers": {
|
||||
"CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}"
|
||||
},
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
{
|
||||
"context_servers": {
|
||||
"custom-server": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -53,10 +52,10 @@
|
||||
{
|
||||
"context_servers": {
|
||||
"custom-server": {
|
||||
"enabled": false,
|
||||
"headers": {
|
||||
"Authorization": "Bearer token"
|
||||
},
|
||||
"type": "http",
|
||||
"url": "https://custom.example.com/mcp"
|
||||
},
|
||||
"everything": {
|
||||
@@ -65,7 +64,7 @@
|
||||
"@modelcontextprotocol/server-everything"
|
||||
],
|
||||
"command": "npx",
|
||||
"enabled": true
|
||||
"type": "stdio"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"context_servers": {
|
||||
"context7": {
|
||||
"enabled": true,
|
||||
"headers": {
|
||||
"CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}"
|
||||
},
|
||||
"type": "http",
|
||||
"url": "https://mcp.context7.com/mcp"
|
||||
},
|
||||
"disabled-server": {
|
||||
@@ -12,7 +12,8 @@
|
||||
"test"
|
||||
],
|
||||
"command": "echo",
|
||||
"enabled": false
|
||||
"enabled": false,
|
||||
"type": "stdio"
|
||||
},
|
||||
"everything": {
|
||||
"args": [
|
||||
@@ -20,7 +21,7 @@
|
||||
"@modelcontextprotocol/server-everything"
|
||||
],
|
||||
"command": "npx",
|
||||
"enabled": true
|
||||
"type": "stdio"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,10 +44,10 @@
|
||||
{
|
||||
"context_servers": {
|
||||
"context7": {
|
||||
"enabled": true,
|
||||
"headers": {
|
||||
"CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}"
|
||||
},
|
||||
"type": "http",
|
||||
"url": "https://mcp.context7.com/mcp"
|
||||
},
|
||||
"disabled-server": {
|
||||
@@ -55,7 +55,8 @@
|
||||
"test"
|
||||
],
|
||||
"command": "echo",
|
||||
"enabled": false
|
||||
"enabled": false,
|
||||
"type": "stdio"
|
||||
},
|
||||
"everything": {
|
||||
"args": [
|
||||
@@ -63,7 +64,7 @@
|
||||
"@modelcontextprotocol/server-everything"
|
||||
],
|
||||
"command": "npx",
|
||||
"enabled": true
|
||||
"type": "stdio"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ let
|
||||
buildScript = ''
|
||||
mkdir -p $out/lib
|
||||
echo "MOCK_NVIDIA_DRIVER_FOR_TESTING" > $out/lib/nvidia-test-marker
|
||||
|
||||
mkdir -p $out/share/egl/egl_external_platform.d/
|
||||
echo "MOCK_NVIDIA_DRIVER_FOR_TESTING" > $out/share/egl/egl_external_platform.d/test.json
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -53,5 +56,7 @@ in
|
||||
markerFile="$envPath/lib/nvidia-test-marker"
|
||||
assertFileExists "$markerFile"
|
||||
assertFileContains "$markerFile" "MOCK_NVIDIA_DRIVER_FOR_TESTING"
|
||||
|
||||
assertFileContains "$confPath" "/etc/egl/egl_external_platform.d/test_nix_gpu.json"
|
||||
'';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user