Compare commits

...
Author SHA1 Message Date
dependabot[bot]andGitHub 621d9b169b ci: bump actions/labeler from 6 to 7
Bumps [actions/labeler](https://github.com/actions/labeler) from 6 to 7.
- [Release notes](https://github.com/actions/labeler/releases)
- [Commits](https://github.com/actions/labeler/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/labeler
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-25 01:52:19 +00:00
dependabot[bot]andAustin Horstman 4ce190229c flake: bump nixpkgs from 74cc63f to 293d6ab
Bumps [nixpkgs](https://github.com/NixOS/nixpkgs) from `74cc63f` to `293d6ab`.
- [Commits](https://github.com/NixOS/nixpkgs/compare/74cc63f702f7d60a557e152a57b40fb1fd0f72ac...293d6abedf0478e681a4dfcfcb35b30fc796a32f)

---
updated-dependencies:
- dependency-name: nixpkgs
  dependency-version: 293d6abedf0478e681a4dfcfcb35b30fc796a32f
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-18 00:01:49 -05:00
dependabot[bot]andAustin Horstman 3cd22efe64 flake: bump nixpkgs from 95ca1e2 to 74cc63f
Bumps [nixpkgs](https://github.com/NixOS/nixpkgs) from `95ca1e2` to `74cc63f`.
- [Commits](https://github.com/NixOS/nixpkgs/compare/95ca1e203c0750115fd4a6f17d5a245dfe6b1edd...74cc63f702f7d60a557e152a57b40fb1fd0f72ac)

---
updated-dependencies:
- dependency-name: nixpkgs
  dependency-version: 74cc63f702f7d60a557e152a57b40fb1fd0f72ac
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-10 22:21:25 -05:00
dependabot[bot]andAustin Horstman af2beae5f0 flake: bump nixpkgs from a037402 to 95ca1e2
Bumps [nixpkgs](https://github.com/NixOS/nixpkgs) from `a037402` to `95ca1e2`.
- [Commits](https://github.com/NixOS/nixpkgs/compare/a0374025a863d007d98e3297f6aa46cc3141c2f0...95ca1e203c0750115fd4a6f17d5a245dfe6b1edd)

---
updated-dependencies:
- dependency-name: nixpkgs
  dependency-version: 95ca1e203c0750115fd4a6f17d5a245dfe6b1edd
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-04 22:14:08 -05:00
Jure VarlecandAustin Horstman 868d0a692d generic-linux-gpu: add EGL support for Nvidia
(cherry picked from commit 4ad9aaae70)
2026-06-28 22:34:17 -05:00
Austin Horstman 7bfff44b46 tests/darwinScrublist: add parallel-full
Scrub the default GNU Parallel package on Darwin so the module test does not build the failing DBD::CSV dependency.

(cherry picked from commit 04063004a3)
2026-06-20 13:45:05 -05:00
dependabot[bot]andAustin Horstman 78671d3ce3 flake: bump nixpkgs from bd0ff2d to a037402
Bumps [nixpkgs](https://github.com/NixOS/nixpkgs) from `bd0ff2d` to `a037402`.
- [Commits](https://github.com/NixOS/nixpkgs/compare/bd0ff2d3eac24699c3664d5966b9ef36f388e2ca...a0374025a863d007d98e3297f6aa46cc3141c2f0)

---
updated-dependencies:
- dependency-name: nixpkgs
  dependency-version: a0374025a863d007d98e3297f6aa46cc3141c2f0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-20 13:45:05 -05:00
dependabot[bot]andAustin Horstman ad026da57b ci: bump actions/checkout from 6 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-20 12:34:54 -05:00
dependabot[bot]andAustin Horstman 8355f0a16b flake: bump nixpkgs from 6b31628 to bd0ff2d
Bumps [nixpkgs](https://github.com/NixOS/nixpkgs) from `6b31628` to `bd0ff2d`.
- [Commits](https://github.com/NixOS/nixpkgs/compare/6b316287bae2ee04c9b93c8c858d930fd07d7338...bd0ff2d3eac24699c3664d5966b9ef36f388e2ca)

---
updated-dependencies:
- dependency-name: nixpkgs
  dependency-version: bd0ff2d3eac24699c3664d5966b9ef36f388e2ca
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-12 22:02:04 -05:00
MalikandAustin Horstman ea6d221d7a news: add entry for programs.mcp typed schema
(cherry picked from commit 1ffdc28076)
2026-06-11 08:25:46 -05:00
MalikandAustin Horstman 005c72cc1a claude-code, codex, antigravity-cli, github-copilot-cli, opencode, vscode, zed-editor: use mcp lib
- use lib.hm.mcp.transformMcpServer for MCP integration
- Update tests to reflect new merged server configuration

(cherry picked from commit ebc55fd103)
2026-06-11 08:25:46 -05:00
MalikandAustin Horstman 22d5ec3d12 mcp: extract helper functions into lib/mcp.nix
Extract MCP-related helper functions (renderEnv, mkEnvFilesWrapper,
wrapEnvFilesCommand, addType, transformMcpServer) into a new shared
library module at modules/lib/mcp.nix.
- Add lib.hm.mcp for sharing transformMcpServer logic across consumers
- Add enabled/disabled conflict detection assertion
- Add tests for new library functions

(cherry picked from commit 6a66db1360)
2026-06-11 08:25:46 -05:00
Austin Horstman d899b01766 opencode: use lib.hm.strings.isPathLike
(cherry picked from commit c321a1c8a7)
2026-06-10 15:56:25 -05:00
Austin Horstman d136d10d61 github-copilot-cli: use lib.hm.strings.isPathLike
(cherry picked from commit 6d4fb02e34)
2026-06-10 15:56:25 -05:00
Austin Horstman 55cf7c9428 codex: use lib.hm.strings.isPathLike
(cherry picked from commit 4b9a995519)
2026-06-10 15:56:25 -05:00
Austin Horstman c88f017a22 claude-code: use lib.hm.strings.isPathLike
(cherry picked from commit 09ff165ba6)
2026-06-10 15:56:25 -05:00
Austin Horstman d48a1e6ad6 antigravity-cli: use lib.hm.strings.isPathLike
(cherry picked from commit 3a647b158e)
2026-06-10 15:56:25 -05:00
Austin Horstman 8982f45135 lib/strings: add isPathLike helper
(cherry picked from commit d33c97ec1b)
2026-06-10 15:56:25 -05:00
Austin Horstman ba6a244f3b antigravity-cli: add migration news
(cherry picked from commit eaca478c95)
2026-06-10 15:56:25 -05:00
Austin Horstman cbc74b9371 antigravity-cli: rename gemini-cli module
(cherry picked from commit ff77a72fe5)
2026-06-10 15:56:25 -05:00
Austin Horstman fcbbf5484a docs: fix sshAuthSock reference in rl-2605
The link to `#opt-sshAuthSock.enable` broke the manual build on
release-26.05, where the `enable` option does not exist. Drop the
broken reference and the inaccurate "enabled and" wording.

(cherry picked from commit 2b394beca7)
2026-06-10 13:10:13 -05:00
Austin Horstman baa46aeb6d docs: update release notes for 26.05
Lots of missing information about major changes throughout the last
release.

(cherry picked from commit c30f177760)
2026-06-09 22:45:20 -05:00
dependabot[bot]andAustin Horstman 4eb4fec416 flake: bump nixpkgs from ec942ba to 6b31628
Bumps [nixpkgs](https://github.com/NixOS/nixpkgs) from `ec942ba` to `6b31628`.
- [Commits](https://github.com/NixOS/nixpkgs/compare/ec942ba042dad5ef097e2ef3a3effc034241f011...6b316287bae2ee04c9b93c8c858d930fd07d7338)

---
updated-dependencies:
- dependency-name: nixpkgs
  dependency-version: 6b316287bae2ee04c9b93c8c858d930fd07d7338
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-07 20:59:21 -05:00
Austin Horstman e28654b710 claude-code: mark hook scripts executable
Inline hooks are scripts, so write them with the executable bit set. Add NMT assertions covering default and custom config directories.

(cherry picked from commit d0af9b8bf3)
2026-06-01 19:47:05 -05:00
dependabot[bot]andAustin Horstman 4baa8ac595 flake: bump nixpkgs from 705e992 to ec942ba
Bumps [nixpkgs](https://github.com/NixOS/nixpkgs) from `705e992` to `ec942ba`.
- [Commits](https://github.com/NixOS/nixpkgs/compare/705e9929918b43bd7b715dc0a878ac870449bb03...ec942ba042dad5ef097e2ef3a3effc034241f011)

---
updated-dependencies:
- dependency-name: nixpkgs
  dependency-version: ec942ba042dad5ef097e2ef3a3effc034241f011
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-01 14:14:08 -05:00
Austin Horstman b179bde238 flake: lock to stable branch
Signed-off-by: Austin Horstman <khaneliman12@gmail.com>
2026-05-25 11:33:45 -05:00
Austin Horstman 0a56fc5a57 release: mark as release branch
Signed-off-by: Austin Horstman <khaneliman12@gmail.com>
2026-05-25 11:32:30 -05:00
95 changed files with 2011 additions and 601 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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 }}
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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 }}
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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: |
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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: |
+4 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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.02.0, previously it erroneously expected values in the
range `0.01.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.02.0, previously it erroneously expected values in the range `0.01.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
View File
@@ -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 -1
View File
@@ -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 =
{
+1
View File
@@ -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; };
+210
View File
@@ -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;
}
+7
View File
@@ -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.
'';
}
+530
View File
@@ -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";
}
];
}
]
);
}
+24 -24
View File
@@ -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
View File
@@ -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";
-314
View File
@@ -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;
}
]
);
}
+47 -29
View File
@@ -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
View File
@@ -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;
};
};
};
}
+30 -26
View File
@@ -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) (
+9 -25
View File
@@ -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 = {
+11 -9
View File
@@ -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
View File
@@ -1,4 +1,4 @@
{
"release": "26.05",
"isReleaseBranch": false
"isReleaseBranch": true
}
+1
View File
@@ -148,6 +148,7 @@ let
"openssh"
"openstackclient"
"papis"
"parallel-full"
"patdiff"
"pay-respects"
"pet"
+1
View File
@@ -178,6 +178,7 @@ import nmtSrc {
[
# keep-sorted start case=no numeric=yes
./lib/generators
./lib/mcp
./lib/types
./modules/files
./modules/home-environment
+3
View File
@@ -0,0 +1,3 @@
{
lib-mcp-wrapper = ./wrapper.nix;
}
+45
View File
@@ -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"
'';
}
@@ -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;
}
@@ -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"'
@@ -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
}
@@ -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 \
@@ -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"
];
}
@@ -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
@@ -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
@@ -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
View File
@@ -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.
''
];
}
+7 -2
View File
@@ -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"
}
}
}
+2 -1
View File
@@ -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"
'';
}