Extensions

The decknix CLI supports user-defined subcommands via a Nix-based extension system.

How It Works

Extensions are defined in Nix and compiled into JSON config files that the Rust binary reads at runtime:

/etc/decknix/extensions.json          ← system-level (from programs.decknix-cli.subtasks)
~/.config/decknix/extensions.json     ← home-level (from decknix.cli.extensions)

Both files are merged. Extensions appear in decknix help and support --help.

Defining Extensions (Home-Manager)

{ ... }: {
  decknix.cli.extensions = {
    board = {
      description = "Issue dashboard across repos";
      command = "${boardScript}/bin/decknix-board";
    };
    cheatsheet = {
      description = "Show WM keybinding cheatsheet";
      command = "${cheatsheetScript}/bin/decknix-cheatsheet";
    };
  };
}

Defining Extensions (System-Level)

# system.nix
{ ... }: {
  programs.decknix-cli.subtasks = {
    cleanup = {
      description = "Garbage collect Nix store";
      command = "nix-collect-garbage -d";
      pinned = true;  # Also creates standalone 'cleanup' command
    };
  };
}

Setting pinned = true creates a standalone wrapper so you can run cleanup directly without the decknix prefix.

Built-in Extensions

Decknix ships with several extensions:

CommandDescription
decknix boardIssue dashboard across GitHub repos
decknix cheatsheetShow window manager keybinding cheatsheet
decknix repo-jdkResolve the JDK a repository declares
decknix spaceSpace picker (GUI)
decknix verifyVerify system integration

decknix board — cross-repo issue dashboard

Prints a compact, colourised dashboard of GitHub issues across your configured repos (open/closed counts per repo, then the open issues with their labels). It is a thin wrapper over gh issue list, so it accepts that command's flags and passes them through per repo:

# The board (open issues across all configured repos)
decknix board

# Filter by label / assignee / search, or show closed issues
decknix board --label enhancement
decknix board --assignee @me
decknix board --state closed --limit 20
decknix board --search "sidebar in:title"

Requires an authenticated gh (gh auth status). The repo set comes from the extension's own configuration.

decknix repo-jdk — per-repository JDK resolution

On a machine running both Nix and SDKMAN, the java that wins on PATH is an accident of shell-init ordering: SDKMAN sources late and prepends its current candidate, shadowing the Nix JDK. A build then runs on a JDK its own build files never asked for, and it surfaces as something unhelpful — an annotation processor rejecting a class file version, say — rather than as "wrong Java".

Pinning a different default does not fix it, because different repositories legitimately need different JDKs. repo-jdk reads what the repository itself declares and resolves that to an installed JDK:

repo-jdk                        # what is declared here, and what is installed
repo-jdk version                # 25
repo-jdk home                   # /nix/store/...-zulu-ca-jdk-25.0.0
eval "$(repo-jdk env)"          # export JAVA_HOME/PATH into the current shell
repo-jdk exec -- ./gradlew build
repo-jdk --dir ~/src/other-repo version

Detection walks up from the working directory to the repository root and takes the first declaration it finds. Within a directory, an explicit toolchain pin beats a build file's language level, because the pin states an install while the build file states a language level:

PrioritySourceExample
1.sdkmanrcjava=25.0.1-zulu
2.tool-versionsjava temurin-21.0.9
3.java-version25
4.mise.tomljava = "17"
5build.gradle.kts / build.gradlejvmToolchain(25), JavaLanguageVersion.of(21), JavaVersion.VERSION_17
6pom.xml<maven.compiler.release>21</maven.compiler.release>

Resolution never downloads anything — it selects from JDKs already on the machine (the Nix installs listed in org.gradle.java.installations.paths, SDKMAN candidates, Gradle's auto-provisioned downloads, and /Library/Java/JavaVirtualMachines), reading each one's release file rather than starting a JVM. Among JDKs matching the declared major it prefers the Nix-provided one, since that is the only origin that reproduces on a fresh machine, then the newest patch within that origin. A version named exactly in .sdkmanrc is honoured verbatim.

It never substitutes a different major: a repo asking for 25 will not silently build on 26.

Exit codes: 3 when nothing is declared, 4 when the declared JDK is not installed (the message lists which majors are).

Tests live beside the script and are plain stdlib unittest:

cd modules/home/options/cli/repo-jdk
python3 -m unittest discover -s . -p 'test_*.py'

Zsh Completion

Extensions automatically get zsh tab-completion. The module generates a completion script that includes all built-in commands plus discovered extensions.

Using Extensions

# Run an extension
decknix board

# Pass arguments
decknix board open --no-color

# Get help
decknix board --help
# Or:
decknix help board

Arguments after the extension name are passed through as $1, $2, etc.