Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

Fnug

CI Crates.io image

Fnug is a TUI command runner that automatically selects and executes lint and test commands based on git changes or file watching. Think of it as a terminal multiplexer (like tmux), but purpose-built for running your dev commands side by side.

Fnug demo

Features

  • Git integration — automatically select commands based on uncommitted, staged or branch changes
  • File watching — monitor the file system and re-select commands when files change, or run them right away
  • Terminal emulation with scrollback — full PTY support for interactive commands and long output
  • Headless mode (fnug check) — run selected commands without the TUI, in a pre-commit hook or in CI
  • Config scaffolding (fnug init) — write a starter .fnug.yaml for the Rust, Python, Node, Go or Nix tooling it finds, or package configs and a workspace root in a monorepo
  • Setup wizard (fnug setup) — install a pre-commit hook that runs fnug check and add the MCP server to your editor
  • MCP server (fnug mcp) — let coding agents list and run your checks
  • Command dependencies — define depends_on to control execution order, and run independent commands in parallel with fnug check --jobs
  • Environment variables — set per-command or per-group env vars
  • Nested command groups — organize commands into a hierarchical tree with inherited settings
  • Workspace support — discover and merge .fnug.yaml files from subdirectories in mono-repos

Installation

Linux and macOS only; commands run via sh -c.

From crates.io

Only prereleases are published so far. cargo install fnug skips prereleases, so name the version explicitly:

cargo install --locked fnug@0.1.0-alpha.14

From PyPI

The latest stable release on PyPI (0.0.x) is the old Python implementation, so allow prereleases:

# With uv
uv tool install --prerelease=allow fnug

# With pipx
pipx install --pip-args=--pre fnug

From GitHub Releases

Download a prebuilt binary from GitHub Releases.

With Nix

# Run directly
nix run github:nickolaj-jepsen/fnug

# Or install to profile
nix profile install github:nickolaj-jepsen/fnug

From source

git clone https://github.com/nickolaj-jepsen/fnug.git
cd fnug
cargo install --path .

Quick start

fnug init    # write a .fnug.yaml for the tooling in this directory
fnug         # open the TUI; Enter runs the commands your changes selected
fnug check   # run them without the TUI instead
fnug setup   # add a pre-commit hook and the MCP server for your editor

Usage

Run fnug in a directory with a .fnug.yaml configuration file, or below one, or pass -c path/to/config.yaml. fnug init creates one.

Subcommands

Command Description
fnug Launch the TUI; see TUI
fnug check [TARGET]... Run selected commands headlessly (useful for pre-commit hooks); see Headless checks
fnug setup Interactive setup wizard for git hooks and MCP server configuration; see Setup
fnug init [DIR] Create a .fnug.yaml with lint and test commands for the project's tooling; see Init
fnug mcp Start an MCP server over stdio; see MCP server
fnug schema Print the config file's JSON Schema; see Editor support
fnug help [COMMAND] Print fnug's help, or a subcommand's, like --help

Global options

Every subcommand takes these, before or after its name: fnug --no-workspace check and fnug check --no-workspace are the same.

Option Description
-c, --config <CONFIG> Path to config file (auto-detected if not specified)
--log-file <LOG_FILE> Also write logs to this file
--log-level <LOG_LEVEL> Log level: off, error, warn, info, debug or trace [default: info, and warn for stderr]
--no-workspace Disable workspace resolution (don't search for a parent workspace root)
--root <DIR> Resolve the config's paths and workspace against DIR instead of the config's directory
-h, --help Print help
-V, --version Print version (fnug -V)

-c always loads the given file as the root, even when a parent workspace root would include it (see Workspace). --root doesn't look for a parent workspace root either, and without -c, fnug searches for the config from DIR instead of the current directory. Init says what -c and --root mean to fnug init; fnug schema ignores them.

Logging

By default, warnings and errors, such as a config that needs a newer fnug, go to stderr in every mode, except while the TUI is open; then they show in its log panel (L). --log-level or the FNUG_LOG environment variable sets the stderr level too: info or debug shows more, and error or off hides warnings. fnug never logs to stdout, which fnug mcp uses for the protocol.

TUI

fnug opens the TUI: the config's groups and commands on the left, and on the right the output of the command under the cursor, each command in a terminal of its own. At startup, and when you press g, fnug selects (●) the auto.always commands and the ones whose auto.git rules match an uncommitted change; with auto.watch, saving a file selects the commands it matches. Enter runs the selected commands, each after its depends_on, and r runs the command or group under the cursor.

Keyboard Shortcuts

Key Context Action
j / ↓ Tree Move down
k / ↑ Tree Move up
h / ← Tree Collapse / deselect
l / → Tree Expand / select
Space Tree Toggle selection
E Tree Expand all groups
W Tree Collapse all groups
/ Tree Search and filter
Enter Tree Run selected commands
r Tree Run command or group
s Tree Stop command
x Tree Clear command
g Tree Select by git changes
F5 Tree Reload the config
w Tree Toggle auto-run
c Tree Copy output
Shift+↑/↓ Tree Scroll output
{ / } Tree Output top / bottom
Shift+Home Tree Output top
Shift+End Tree Output bottom
Tab Tree Type into the command
Ctrl+R Tree Toggle fullscreen
L Tree Toggle log panel
? Tree Toggle this help
q / Ctrl+C Tree Quit
Enter Search Keep the filter
Esc Search Clear the search
Ctrl+] Terminal Back to the tree
Esc Terminal Back, unless full-screen
Esc Fullscreen Exit fullscreen

? shows this list in the TUI; j and k scroll it when it doesn't fit. The toolbar at the bottom shows the shortcuts for the item under the cursor, the most needed first when they don't all fit, and always ? Help at its right edge; click one to use it.

Tab gives the keyboard to the command under the cursor while it runs, so you can answer a prompt or use a full-screen program. While a command has the keyboard, every key but Ctrl+] goes to it, Ctrl+C and Ctrl+R included, and so does Esc if it runs a full-screen program. The keyboard returns to the tree when the command exits.

Mouse

  • Click a tree item to move the cursor to it
  • Double-click a command to run it, or a group to expand/collapse
  • Click the selection orb (●/○) or arrow (▼/▶) to toggle
  • Click a toolbar shortcut to use it
  • Drag the separator between tree and terminal to resize
  • Scroll wheel over the tree to scroll it, and over the terminal panel to scroll output, or to scroll in a program that uses the mouse
  • Click or drag the terminal panel's rightmost column to jump through the scrollback
  • Click the terminal panel of a running full-screen or mouse-aware program to type into it
  • Right-click a group, a command or the terminal panel for a context menu: run, restart, stop, copy output, clear and more

Copying output

c copies the command's whole output, scrollback included, without the lines fnug adds before and after it. When the command's latest queued run never started, because you stopped it or a command it depends on, c copies nothing and says so, as the pane shows no output then. fnug uses pbcopy on macOS and wl-copy, xclip or xsel on Linux, and falls back to OSC 52, an escape sequence that asks the terminal fnug runs in to set the clipboard. Over SSH, OSC 52 comes first, so the text lands on your machine rather than the server. Inside tmux, OSC 52 needs set -g set-clipboard on. Output over 1 MiB is cut to its last 1 MiB. Some terminals limit the size of an OSC 52 copy and silently drop a larger one, and fnug can't tell when that happens.

Reloading the config

The TUI loads the config again when you save one of its files, or when you press F5. Commands and groups keep their output, selection and expansion by id. A command that is no longer in the config is stopped, and a running command whose cmd, cwd or env now differs from what it was started with keeps running as it was until you restart it; the toolbar names it. While a fnug check --stash runs, reloads wait for it to end (see Checking what is committed). If the new config doesn't load, the old one stays and the toolbar shows why until a reload succeeds. A new workspace package is only picked up by F5, since fnug watches the config files it loaded.

Headless checks

fnug check runs the selected commands without the TUI, prints how each one ended, and exits with a code that says whether they all passed, for a pre-commit hook, CI or a script.

Options

Option Description
[TARGET]... Run these commands, by id or name, after their dependencies, instead of the ones changes select. They run even with auto.check: false
--all Run every command instead of the ones changes select, except those with auto.check: false unless --include-manual is given
--include-manual Also run commands with auto.check: false
--base <REF> Select by the changes since the merge base of HEAD and REF, such as origin/main: commits since then plus uncommitted changes
--staged Select by the changes staged for the next commit; unstaged and untracked changes don't count. In a pre-commit hook, the index git is committing
--stash With --staged: set unstaged changes to tracked files aside while commands run, so they check exactly what is staged, and put them back afterwards
--fail-fast Stop on first failure
--no-tui Never prompt to open the TUI on failure
--mute-success Suppress stdout/stderr for commands that pass
--timeout <DURATION> Kill commands that run longer than DURATION (seconds, or e.g. 90s, 5m), unless their config sets timeout
-j, --jobs <N> Run up to N commands at once, each after its dependencies (0: one per CPU). Above 1, output is captured and printed as each command finishes [default: 1]
--allow-modifications Let commands change tracked files, such as a formatter that fixes what it finds, without failing

Targets, --all, --base and --staged each decide what runs, so only one of them can be given. --include-manual doesn't go with targets, which run even with auto.check: false, and --stash needs --staged. --timeout 0 means no limit.

What fnug check runs

By default, fnug check runs the always commands and the commands whose auto rules match an uncommitted change (staged, unstaged or untracked), each after its depends_on. Other ways to choose:

Invocation Runs
fnug check --staged The always commands and those the staged changes select
fnug check --base origin/main The always commands and those the changes since the merge base of HEAD and origin/main select
fnug check --all Every command except auto.check: false ones
fnug check --all --include-manual Every command
fnug check lint "unit tests" The named commands, by id or name

In a pre-commit hook, the staged changes are the ones being committed, also with git commit -a and git commit <path>. The changes since the merge base are the branch's commits plus uncommitted changes.

Commands with auto.check: false are left out unless you add --include-manual or name them. When nothing is selected, fnug says so and exits with 0: No commands selected (12 configured; use --all, --base <ref>, or name commands). With --staged, as in the pre-commit hook, it leaves out the hint and prints No commands selected (12 configured), so a commit that only touches docs passes with one line of output.

--base compares in the git repository that contains the current directory, not the config file's, so fnug -c /elsewhere/fnug.yaml check --base origin/main works when run inside the repository. The base has to resolve there even when no command has auto.git, so a typo in it fails the run instead of running only the always commands. In a workspace whose packages are other repositories, such as submodules, a package repository where the base doesn't resolve, or that has no commits yet, gets a warning and none of its commands are selected; the rest still run. When the current directory is outside every repository that auto.git commands' paths are in, as at the root of a workspace that isn't a repository but whose packages each are, the base must resolve in each of those repositories instead.

Output

fnug check prints a line for each command, such as PASS, FAIL (exit 3), TIMEOUT after 5m, SKIP (build failed), CANCELLED or NOT RUN, then a summary that counts every selected command once: passed, failed, timed out, skipped because a dependency failed, cancelled, and not run. When --fail-fast stops a run, commands still running are stopped and counted as cancelled, and commands not yet started as not run. Without --mute-success or --stash, commands share fnug's terminal and their output streams through. With --mute-success, fnug prints [i/N] name when a command starts and its result when it ends, followed by the command's output, captured with stdout and stderr merged in order, if it didn't pass. --stash alone does the same, but prints the output of commands that pass too. --jobs above 1 captures output the same way and prints each command's result line and output, unless it passed and --mute-success is set, as soon as the command finishes. Commands still start in config order once their dependencies pass, and an exclusive one waits until nothing else runs and holds back the rest until it ends.

When the run fails and both stdin and stderr are terminals, fnug check asks Open TUI to investigate? [y/N]. Answer y to open the TUI, which selects the commands that failed, timed out or were skipped and runs them again. --no-tui never asks.

Exit codes

Exit code Meaning
0 Every command passed, or none was selected
1 A command failed, timed out or changed tracked files, or was skipped or not run
2 fnug couldn't do its job (see below)
128+n Stopped by signal n

Exit code 2 means a usage error, a config that doesn't load, an unknown or ambiguous name, a --base that doesn't resolve in the current directory's repository, --staged outside a git repository, --base when neither the current directory nor an auto.git command's path is in one, or unstaged changes that --stash couldn't set aside or put back.

The TUI exits with the same codes: 128+n when a signal stops it, 2 when it fails, such as when it can no longer read from or draw to its terminal, and 0 when you quit. When fnug check opens the TUI after a failure, quitting it keeps the check's 1.

CI usage

A fresh CI checkout has no uncommitted changes, so plain fnug check selects nothing there. In a pull request, compare with the target branch with --base; elsewhere, run everything with --all. --base needs the merge base in the clone, so fetch the whole history: in a shallow clone, fnug check --base exits with 2 and says it found no common history. In GitHub Actions, after installing fnug (see Installation):

- uses: actions/checkout@v5
  with:
    fetch-depth: 0
- if: github.event_name == 'pull_request'
  run: fnug check --base "origin/${GITHUB_BASE_REF:-main}"
- if: github.event_name != 'pull_request'
  run: fnug check --all

Without a terminal, fnug check never offers to open the TUI, and runs each command in a session of its own (see Commands without a terminal). Add --jobs 0 to run independent commands at once, one per CPU, and --timeout to stop a command that hangs. Exit code 1 means a check failed, and 2 that fnug couldn't run the checks (see Exit codes).

If the checkout belongs to another user than the one running fnug, as in some containers, fnug refuses the config it finds until you set FNUG_SAFE_DIRECTORIES (see Trusted configs).

Commands that change files

fnug check fails a command that exits with 0 but changes tracked files, and prints FAIL (modified: src/a.rs — review and re-stage). Otherwise a formatter that rewrites files would pass while the commit keeps the unformatted version. Run the checking form in check mode (cargo fmt --check, ruff format --check), give fixers auto.check: false so only the TUI runs them, or pass --allow-modifications. New untracked files and ignored files don't count. With --jobs above 1, a change counts against every command that was running when it happened; the run fails either way. The TUI and the MCP server let commands change files.

Checking what is committed

--staged decides which commands run, but they still see the work tree, with edits you haven't staged. Add --stash to check exactly what is staged: fnug saves the unstaged changes to tracked files as a patch in the git directory, checks the tracked files out from the index, and applies the patch again once the commands have exited. The pre-commit hook that fnug setup installs runs fnug check --staged --stash --fail-fast --mute-success --jobs 0.

  • Untracked files stay where they are, and files added with git add -N stay intent-to-add.
  • It works on the repository that contains the current directory, as --staged does, not the one that contains the config file. It covers that whole repository, even when the hook runs fnug from a subdirectory, and works the same in a linked worktree, a submodule or a repository with a separate git directory. In a workspace, other repositories, such as submodules, aren't touched.
  • When a command changes a file that also has unstaged changes, as a formatter can, fnug always discards the command's changes to that file when it puts yours back, even when the two don't overlap, and says so: Commands changed files that also have unstaged changes, so their changes to those files were discarded to put yours back. Its changes to files without unstaged changes stay. Unless you pass --allow-modifications, the command has already failed for changing files.
  • fnug deletes the patch once the files it changes hold exactly your changes again. Otherwise it keeps the patch and prints its path.
  • A TUI open in the same worktree waits while a fnug check --stash runs, such as the pre-commit hook: the files the checkout and the restore change don't select commands or start auto.run_on_change runs, a config reload (from a saved config file or F5) is put off, and the toolbar says Waiting for fnug check --stash to finish. Once the run has ended, which the TUI checks about once a second, it reloads the config once if a config file changed, then selects what the changed files select and auto-runs those commands, on the final content.
  • A signal doesn't stop fnug before it has put the changes back. If fnug is killed with SIGKILL, the next fnug check --staged --stash in that worktree puts them back first. When the files they change have changed since, it keeps the patch, says how to apply it by hand and exits with 2. Until then, other fnug check runs warn that changes are still set aside. Only one such run works on a worktree at a time; another one exits with 2. Linked worktrees of one repository don't wait for each other.

Commands without a terminal

Captured commands, from --mute-success, --stash, --jobs above 1, the pre-commit hook or an MCP run, have no terminal. Their stdin is /dev/null, and each runs in a session of its own, so opening /dev/tty, as password and host-key prompts do, fails at once instead of waiting for input that never comes. When a captured command's shell exits, fnug stops whatever the command left running in its process group, since such a process would keep the output pipe open; start a process that should keep running in a session of its own, for example with setsid. Captured output keeps the first 256 KiB and the last 1 MiB of what a command writes, with … N bytes omitted … in between. MCP runs keep the first 64 KiB and the last 256 KiB, of which a result shows less (see MCP server).

When none of fnug's stdin, stdout and stderr is a terminal, as in CI, streamed commands run in a session of their own too, and fnug stops what they leave running in the same way. A pipe on stdout alone isn't enough: fnug check | tee check.log from a shell keeps the terminal on stdin and stderr. From a terminal, a streamed command stays in fnug's process group so it can use the terminal, and fnug signals only the command's own process: a process it started, such as a test binary under cargo test, can outlive a timeout or a signal and keep writing to the terminal. Use --mute-success or --jobs for timeouts that stop the whole process tree. --stash always captures output for the same reason, so that nothing a command started writes to files once your unstaged changes are back.

Signals

On SIGINT, SIGTERM or SIGHUP, fnug check stops the running commands, prints the summary so far, marked Interrupted., and exits with 128 plus the signal number. On Ctrl+C (SIGINT), a command that shares fnug's terminal has already got the terminal's SIGINT, so fnug gives it 3 s to finish before sending SIGTERM, while a command in a session of its own gets SIGINT from fnug. On SIGTERM or SIGHUP, commands get the same signal. A timeout, --fail-fast and an MCP client's cancellation send SIGTERM. Whichever signal a command gets, it gets SIGKILL if it is still running 3 s later. Signals reach the whole process group of a command in a session of its own, but only the process of a command that shares fnug's terminal.

Init

fnug init writes a .fnug.yaml in the current directory, or in the directory you pass, with a group of commands for each kind of tooling it finds there.

Argument / option Description
[DIR] Directory to create the config in [default: --root, -c's directory, or the working directory]
--force Replace the directory's config if it has one
-y, --yes Include everything detected without asking; the default when stdin isn't a terminal
Found Commands
Cargo.toml cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo test
pyproject.toml, ruff.toml ruff check ., ruff format --check ., mypy ., pytest
package.json Its format:check, lint, typecheck and test scripts
go.mod A gofmt -l check, go vet ./..., go test ./...
flake.nix alejandra --check ., statix check ., deadnix --fail .
  • Python: mypy . and pytest only if pyproject.toml configures or depends on them (or mypy.ini, pytest.ini or conftest.py exists). The commands are prefixed with uv run, poetry run or pdm run when that tool's lockfile exists.
  • Node: the scripts run with pnpm, yarn, bun or npm to match the lockfile. test gets CI=true so test runners don't start in watch mode, and is left out if it is the placeholder npm init writes.
  • Go: the gofmt check fails if gofmt fails or lists any file. Like go vet ./..., it skips vendor/, testdata/ and directories whose names start with . or _.
  • Nix: each tool only if it is on PATH.

Each group's commands are selected by uncommitted changes to the files they check (auto.git) and by edits to them while fnug runs (auto.watch). fnug init asks which groups to include; --yes, or a stdin that isn't a terminal, includes them all. If it finds nothing, the config has an example command to replace. The file starts with a comment that points the YAML language server at the schema for your fnug version (see Editor support) and sets fnug_version.

When the directory itself has none of this tooling, but two or more directories below it are packages, because they have such tooling or a config already, fnug init offers a config for each package that has none, plus workspace: true at the top, which also takes in the packages configured already (see Workspace). It looks up to three levels down, skipping hidden and gitignored directories, node_modules, vendor and target, and doesn't look inside a package. The prompt names each group with its package's path, such as Rust (crates/core/Cargo.toml): fmt, clippy, test, and --yes takes them all. fnug prints the root config and each package config it created. With fewer than two packages, or when you pick nothing and no package has a config yet, it writes the example config instead. --force in a monorepo that is set up already writes the root config as a workspace root again.

It never replaces an existing config unless you pass --force, which rewrites the config fnug loads from that directory in its own format. Run it in a workspace package's directory to give that package a config of its own. Below a parent config that isn't a workspace root, it warns that fnug run there will load only the new config; add workspace: true to the parent config to load both, with the new one as a package.

Without a directory argument, the config goes in --root, as fnug setup's offer does, or else in the directory of the file -c names, or else in the current directory. -c FILE names the file to write, in JSON if it ends in .json and in YAML otherwise. A config runs its commands from its own directory unless it is loaded with --root, so a -c file outside the project directory needs --root as well; without it, fnug init exits with 2, as it does when the directory argument and --root differ. --no-workspace has no effect on fnug init.

Setup

fnug setup installs a git pre-commit hook that runs fnug check --staged --stash --fail-fast --mute-success --jobs 0, which checks what is being committed (see Checking what is committed), and adds the MCP server to your editors' project config: .mcp.json for Claude Code, .vscode/mcp.json and .cursor/mcp.json. It needs a terminal to ask on, lists every change before making any, and makes them only once you confirm. Deselect something to remove it. When neither the directory nor a parent has a config, setup first offers to create what fnug init would: a config with the groups it proposes for the directory's own tooling, or in a monorepo a workspace root and a config for each package (see Init), or else a config with an example command. It writes them before the hook and the editor entries that run them. If you leave it unticked, the list of changes notes that the hook fails every commit, and every call to the MCP server fails, until there is a config. When a config fails to load, here or in a parent, setup offers none and warns instead.

The hook goes where git reads hooks: in core.hooksPath if that is set, otherwise in the main repository's .git/hooks, which linked worktrees share. If husky manages the hooks, or core.hooksPath points outside the repository, setup prints the lines to add yourself instead. A pre-commit that is a symlink counts as the file it links to: setup edits that file, shows it in the list of changes, and prints the lines instead if it is outside the repository.

fnug's lines sit between # >>> fnug >>> and # <<< fnug <<<, right after the shebang of any existing hook, and the rest of that hook runs after fnug passes. If fnug needs something your hook sets up first, such as PATH, move the block below it; updates leave it where it is. A hook that isn't a shell script, such as a Python one, can be chained instead: it moves to pre-commit.local and runs after fnug, and removing fnug's hook puts it back.

The hook runs fnug from PATH, or else the binary that ran fnug setup, with the -c, --root and --no-workspace that fnug setup was given. A different or older fnug on PATH still comes first, so setup tells you when the one on its PATH isn't the binary running setup: one from before --staged and --stash fails every commit. It runs from the config's directory. With --root, it runs from that directory instead, and goes in that directory's repository even when the config is in another one. In a linked worktree, a submodule or a repository with a separate git directory, git gives the hook a GIT_DIR but no GIT_WORK_TREE; the hook then sets GIT_WORK_TREE to the top before it changes directory, so commands that run git still see the whole work tree. Run setup again after moving the config, and it offers to update the hook. If the hook runs a different config that still exists, as when you run setup from a nested config or with other flags, setup leaves it alone, even when you deselect the hook, and says which config it runs; it repoints the hook only if you say yes when asked. If fnug isn't installed, the commit fails with a hint; a hook in the work tree, such as one in a committed core.hooksPath or a committed script that .git/hooks/pre-commit links to, only warns, so teammates without fnug can still commit.

In a workspace, setup also offers a hook for each package whose config is in another repository, such as a git submodule. That hook runs the package's checks with --no-workspace.

Setup edits the editor configs in place, keeping comments and formatting, and adds or removes only the fnug entry. The entry runs fnug mcp with the -c, --root and --no-workspace that fnug setup was given, with paths relative to the project directory the editor starts it in, since the editor config is usually committed. fnug has to be on the editor's PATH. Run setup again with other flags, and it offers to update the entry.

MCP server

fnug mcp lets coding agents run your checks through the Model Context Protocol, over stdio. fnug setup adds it to Claude Code, VS Code and Cursor; other clients can start fnug mcp in the project directory. It has four tools:

Tool Parameters Description
list_lints group, name, auto_type, base List the commands, optionally filtered, and whether the current changes select them
run_lints base, include_manual, fail_fast, jobs, timeout_secs, verbose Run the commands the current git changes select, as fnug check does
run_lint command, timeout_secs, verbose Run one command by id or name, after its dependencies
run_all include_manual, fail_fast, jobs, timeout_secs, verbose Run every command except those with auto.check: false

list_lints filters by a substring of the group name (group) or of the command's name or id (name), and by auto_type: git, watch, always or none.

The server loads the config again on every tool call, so edits to it apply without a restart. Its warnings, such as an empty group or a fnug_version mismatch, go to stderr once at startup and again only when a reload gives different ones. It also starts when the config is missing or broken; each tool call then fails with the reason until the config is fixed.

Like fnug check, run_lints and run_all skip commands with auto.check: false unless include_manual is set; run_lint runs any command. It takes a command's id, or a name that only one command has, ignoring case. When an id wins but other commands have that name, the result's message names them, such as 'fmt' is also the name of rust-fmt (fnug > rust), so you can run the one you meant by its id. run_lints looks at uncommitted changes, or with base (such as origin/main) at everything that changed since the merge base with that revision, commits included. As with fnug check --base, the base is looked up in the git repository that contains the server's working directory: a base that doesn't resolve there, or any base when neither that directory nor an auto.git command's path is in a git work tree, makes run_lints and list_lints return an error, even when no command has auto.git. When it selects nothing, the result's message says why: no changed files, changes that no command's auto rules match, or only auto.check: false commands selected. Each command in a result has a reason: requested, all, always, git or dependency of …, and a git-selected one lists up to five matched_files. list_lints shows the same for what the changes select now, takes base too, and marks commands with auto.check: false with runs_in_check: false.

A run returns a compact JSON summary, then a text block for each command that failed or timed out. The summary's message sums the run up in a sentence or two, and commands lists every planned command, failures first, with its status (passed, failed, timeout, skipped, cancelled or not_run), exit code, duration and output size. A text block holds the command's output with stdout and stderr merged in order and terminal escapes and carriage-return overwrites removed, cut to its first 4 KiB and last 16 KiB; truncated_bytes in the summary says how much was left out. The text blocks together, their ### headers and … N bytes omitted … lines included, take at most 60 KiB. Failures get the room first; once too little is left for a block to keep about 512 bytes of output, that command and the ones after it get no block, and the summary's message names them (run_lint shows their output). The summary stays within 16 KiB: past that, entries at the end of commands, where passes come, are left out, and commands_omitted and the message say how many. It lists at most 10 warnings, and a failure for changing tracked files names at most 5 of them. Set verbose to also get the output of commands that passed.

Runs take turns: a run tool called while another run is in progress waits for it to finish, and its summary's queued_ms says for how long. As with fnug check --timeout and --jobs, timeout_secs stops a command that runs longer than that many seconds unless its config sets timeout, and run_lints and run_all take jobs to run up to that many commands at once. Unlike fnug check, runs default to a 10-minute limit (timeout_secs: 0 for none) and to one command per CPU (jobs: 1 for one at a time), since an agent can't interrupt a run and gets the output only when it ends.

Cancelling a tool call stops its commands with SIGTERM, or stops it waiting for its turn. When the client closes the server's stdin, the server does the same for every running call, waits up to 10 s for the commands to exit and then exits. On SIGINT, SIGTERM or SIGHUP, running commands get the same signal and the server exits with 128 plus its number. A command still running 3 s after its signal gets SIGKILL.

Python wrapper

The PyPI package (see Installation) ships the fnug binary and a small Python module around it, for running checks from a script or generating a config in code. It needs Python 3.10 or later.

import fnug
from fnug import Auto, Command, CommandGroup, Config

# The .fnug.yaml found from the working directory, as `fnug check --staged --fail-fast`
result = fnug.check(staged=True, fail_fast=True)
print(result.returncode)  # 0: passed, 1: a check failed, 2: fnug couldn't run them

config = Config(
    name="my-project",
    children=[
        CommandGroup(
            name="python",
            auto=Auto(git=True, path=["./src"], regex=[r"\.py$"]),
            commands=[
                Command(name="ruff", cmd="ruff check {files}"),
                Command(name="pytest", cmd="pytest", depends_on=["ruff"]),
            ],
        ),
    ],
)
fnug.check(config, all_=True)  # run a config that only exists in memory
fnug.start(config)             # or open it in the TUI
config.write(".fnug.yaml")     # or save it: JSON for a .json path, YAML otherwise
  • fnug.check() runs fnug check, with its options as keyword arguments: targets, all_, include_manual, base, staged, stash, fail_fast, no_tui, mute_success, jobs, timeout and allow_modifications.
  • fnug.start() opens the TUI.
  • Both take the global options config_path, root, no_workspace, log_file and log_level, or a Config as their first argument instead of config_path. They return the subprocess.CompletedProcess, with the output left on the terminal, and don't raise when a check fails.
  • fnug.run(*args) runs the binary with any arguments.

They run the fnug next to the Python interpreter, where the wheel installs it, or else the first one on PATH.

Config, CommandGroup, Command, Auto and WorkspaceOptions are dataclasses with keyword-only fields named after the config keys (see Configuration reference); Config.schema is written as $schema. to_dict(), to_yaml(), to_json() and write(path) leave out the fields that are None. fnug_version defaults to the installed package's version in the form the binary reports, so 0.1.0a13 from PyPI becomes 0.1.0-alpha.13. A Config passed to check() or start() is written to a temporary JSON file, and fnug gets --root set to the working directory, or to root if given, so the config's paths, commands and workspace work as they would in a .fnug.yaml there.

Configuration

Fnug searches for .fnug.yaml, .fnug.yml, or .fnug.json from the current directory upward. In a directory with more than one, .fnug.json wins, then .fnug.yaml.

Unknown keys are errors, so a typo like depends-on or gti fails loudly with its line number and a suggestion instead of being ignored. YAML anchors and aliases work (auto: *defaults), but merge keys (<<: *defaults) are not supported.

Minimal example

fnug_version: 0.1.0
name: my-project
commands:
  - name: hello
    cmd: echo world

Git auto-selection

Select commands based on uncommitted changes: in the TUI at startup and when you press g, and in fnug check, which can look at the staged changes or a branch's changes instead (see What fnug check runs).

fnug_version: 0.1.0
name: my-project
commands:
  - name: lint
    cmd: cargo clippy
    auto:
      git: true
      path:
        - "./src"
      regex:
        - "\\.rs$"

File watching

While the TUI runs, monitor the file system and select commands when matching files change. Can be combined with git auto.

fnug_version: 0.1.0
name: my-project
commands:
  - name: test
    cmd: cargo test
    auto:
      watch: true
      path:
        - "./src"
      regex:
        - "\\.rs$"

Add run_on_change: true to also run the command when a change selects it, as bacon or cargo-watch do. It needs watch: true too: without it no change selects the command, so it never runs on its own. Like the other auto keys, it is inherited by child groups and commands. A change while the command is queued or running gets it one more run once that run ends, and changes in the second after an automatic run ends are ignored, so the files a formatter or clippy --fix writes don't start it again right away. A change during the run can still start one extra run. Press w in the TUI to turn this off, and on again, for the session; the toolbar shows w Auto-run only when some command has both watch and run_on_change.

Always auto-selection

Mark commands that should always be selected, regardless of git changes or file watching.

fnug_version: 0.1.0
name: my-project
commands:
  - name: typecheck
    cmd: cargo check
    auto:
      always: true

Excluding commands from check mode

Commands with auto.check: false are skipped during fnug check, including fnug check --all, git hooks and the MCP run_lints/run_all tools, but remain auto-selected in the TUI. Use fnug check --include-manual or the MCP tools' include_manual to include them, name them (fnug check "integration tests"), or use MCP run_lint to run one by id or name. A command that another command depends on runs with it either way.

Useful for commands that are too slow or noisy for pre-commit checks but you still want to run them automatically in the TUI.

fnug_version: 0.1.0
name: my-project
commands:
  - name: unit tests
    cmd: cargo test
    auto:
      git: true
  - name: integration tests
    cmd: cargo test --release
    auto:
      git: true
      check: false   # skip in `fnug check`, still auto-selected in TUI

Nested groups with inheritance

Groups inherit cwd, auto, env, timeout and exclusive from their parent. Each auto field is inherited on its own, so a group's check: false applies to every command below it unless a command sets check itself.

fnug_version: 0.1.0
name: my-project
children:
  - name: backend
    auto:
      git: true
      watch: true
      path:
        - "./src"
      regex:
        - "\\.rs$"
    commands:
      - name: fmt
        cmd: cargo fmt --check
      - name: test
        cmd: cargo test
      - name: clippy
        cmd: cargo clippy

auto.path and auto.regex are inherited separately and both must match, so a command that narrows path keeps the group's regex. Set regex: [] to drop an inherited regex (any file under path matches), or path: [] to reset path to the command's own directory:

fnug_version: 0.1.0
name: my-project
children:
  - name: rust
    auto:
      git: true
      path: ["./src"]
      regex: ["\\.rs$"]
    commands:
      - name: test
        cmd: cargo test
      - name: lockfile
        cmd: cargo check --locked
        auto:
          path: ["./Cargo.toml"]
          regex: []

Environment variables

env adds environment variables to every command in a group, or to one command, on top of the ones it inherits. In a value, $VAR and ${VAR} expand to the inherited value of VAR, or else to fnug's own environment; an unset variable expands to nothing, with a warning. Write $$ for a literal $. Variables in the same env map don't see each other.

fnug_version: 0.1.0
name: my-project
env:
  PATH: "./node_modules/.bin:$PATH"
commands:
  - name: lint
    cmd: eslint .
    env:
      ESLINT_CACHE: "${HOME}/.cache/eslint"

Matched files

A command that fnug check or MCP run_lints selects through auto.git gets the changed files that selected it in FNUG_FILES, one per line, relative to its cwd when they are inside it (with ./ in front of a path that starts with -) and absolute otherwise. Files that no longer exist are left out. {files} in cmd becomes the same list, each path in single quotes for the shell:

fnug_version: 0.1.0
name: my-project
commands:
  - name: ruff
    cmd: ruff check {files}
    auto:
      git: true
      path: ["./python"]
      regex: ["\\.py$"]

A command without such a list gets no FNUG_FILES, and {files} becomes its auto.path entries (. for its own cwd, or when it has none), so it checks everything it covers. That happens when you name it or pass --all, when it runs only as another command's dependency, when it has no auto.git, when all its matched files were deleted, and, with a warning, when the list is longer than 100 KiB.

In the TUI, a command that git selected gets the files git matched when you run it (Enter, r or a group run), and one the file watcher selected gets the files that changed since it last passed, auto.run_on_change runs included; a change while it runs adds to the files of the rerun queued for it. When the watcher selects a command that git selected, the changed files are added to the ones git matched. A command you selected by hand, one selected by auto.always or by a failed fnug check that opened the TUI, one that isn't selected, and one that runs only as another's dependency get the auto.path fallback.

Ids and dependencies

Every command and group has an id, which depends_on, fnug check targets and the MCP tools use, and by which the TUI keeps a command's output across config reloads. It defaults to the name, with / replaced by -. When several commands or groups would get the same default id, each of them gets its group path instead, such as backend/test and frontend/test. Set id to choose one yourself; explicit ids must be unique and can't contain /. If two still end up with the same id (siblings with the same name, or a command and a group with the same name in one group), fnug reports an error naming both; set id on one of them.

fnug resolves a depends_on entry within the command's config file, using the first of these that matches:

  1. an id, or the name of one of the command's siblings;
  2. a group path like backend/test;
  3. a name that is unique in the file;
  4. the full id of a command in another workspace package or the root, like api/build.

If an entry is the id of one command and also the name of a sibling, fnug reports an error rather than guessing. Use the sibling's group path, or give the other command a different id.

fnug_version: 0.1.0
name: my-project
children:
  - name: backend
    commands:
      - name: build
        cmd: cargo build
      - name: test
        cmd: cargo test
        depends_on: [build]   # the sibling: backend/build
  - name: frontend
    commands:
      - name: build
        cmd: pnpm build
      - name: test
        cmd: pnpm test
        depends_on: [build]   # frontend/build

Workspace

Workspace mode discovers .fnug.yaml files (or .fnug.yml, .fnug.json) in subdirectories and merges them as child groups. This is useful for mono-repos where each package has its own config.

When workspace: true, fnug walks the filesystem (skipping .gitignore'd and hidden directories) to find sub-configs. Files do not need to be git-tracked to be discovered. Outside a git repository nothing counts as ignored, so only hidden directories are skipped. The walk doesn't look inside a directory that has a config, and a package's own workspace key is ignored with a warning, so packages don't nest.

# Auto-discover sub-configs (walks up to 5 levels deep)
fnug_version: 0.1.0
name: my-monorepo
workspace: true
commands:
  - name: root-lint
    cmd: echo "root"
# Custom max scan depth
workspace:
  max_depth: 2
# Explicit glob patterns
workspace:
  paths:
    - "./packages/*/"
    - "./apps/*/"

Each package behaves the same as when fnug runs inside it on its own: its cwd and auto.path are relative to the package directory, and it inherits none of the root config's cwd, auto, env, timeout or exclusive. Package ids are prefixed with the package's id, which defaults to its name, so a build command in a package named api has the id api/build. Package ids must be unique across the workspace and must not match a root group's id. Inside a package, depends_on: [build] means the package's own build; reference another package's command by its full id (api/build) and a root command by its id.

When fnug finds a config by searching upward (no -c), it loads a parent workspace root instead if that root's own discovery includes the config. A config the root doesn't discover, for example in a gitignored or hidden directory, below max_depth, or not matched by paths, is loaded on its own. Parent configs that fail to parse, or whose discovery fails, are skipped with a warning. -c always loads the given file as the root. Use --no-workspace to never look for a parent workspace root.

Trusted configs

A config runs commands as you, so fnug only loads a config it found by itself (in the current directory or a parent, as a parent workspace root, or as a workspace package) if the file is owned by you or by root, much like git's safe.directory. A config passed with -c is always loaded. If the nearest config belongs to another user, fnug stops with an error; an untrusted parent workspace root or package is skipped with a warning.

To trust configs owned by someone else, for example in a CI container where the checkout belongs to a different user, list their directories in FNUG_SAFE_DIRECTORIES, separated by :. Set it to * to trust every config.

Running fnug as root, for example with sudo, trusts only configs owned by root, so your own repository's config is refused. Pass it with -c instead.

Advanced example

See this project's .fnug.yaml for a full example.

Editor support

fnug publishes a JSON Schema for config files, so editors can complete keys and flag mistakes. With the YAML language server (VS Code's YAML extension, Neovim's yamlls), add this as the first line of .fnug.yaml:

# yaml-language-server: $schema=https://raw.githubusercontent.com/nickolaj-jepsen/fnug/main/schema/fnug.schema.json

In .fnug.json, use a "$schema" key with the same URL. fnug schema prints the schema for the installed version.

Configuration reference

Every key the schema (fnug schema) allows. "Inherited" keys also apply to everything below, unless a group or command sets them itself.

Root fields

Field Type Default Description
name string required Display name for the root group
fnug_version string none fnug version the config is written for (see below)
id string the name Id of the root group; in a workspace package, the prefix of its ids
workspace bool / object false Discover and merge package configs (see Workspace fields)
commands list of commands none Top-level commands
children list of groups none Command groups
cwd string the config's directory, or --root Working directory, relative to the config's directory or --root (inherited)
env map of strings none Environment variables for every command, $VAR expanded (inherited)
auto object none Default auto rules for every command (inherited)
timeout integer / string no limit Default command timeout (inherited)
exclusive bool false Default command exclusive (inherited)
$schema string none JSON Schema URL for editors (mainly for .fnug.json); ignored

fnug_version compares only the major.minor.patch numbers, so 0.1.0 matches 0.1.0-alpha.13. fnug warns when the config needs a newer fnug, when it was written for an older release series (a different minor version before 1.0, a different major version after), or when the version can't be parsed. Without it, fnug doesn't check.

Group fields

Field Type Default Description
name string required Display name
id string the name Identifier (see Ids and dependencies)
cwd string the parent's Working directory, relative to the parent group's (inherited)
env map of strings none Environment variables, added to the inherited ones, $VAR expanded (inherited)
auto object the parent's Default auto rules, each field on its own (inherited)
timeout integer / string the parent's Default command timeout (inherited)
exclusive bool the parent's Default command exclusive (inherited)
commands list of commands none Commands in the group
children list of groups none Nested groups

Command fields

Field Type Default Description
name string required Display name
cmd string required Shell command, run with sh -c
id string the name Identifier for depends_on, fnug check targets and the MCP tools (see Ids and dependencies)
cwd string the group's Working directory, relative to the group's
env map of strings none Environment variables, added to the inherited ones, $VAR expanded
auto object the group's Auto-selection rules, each field on its own (see Auto fields)
depends_on list of strings none Commands that must pass before this one runs, by id or name
scrollback integer 3500 Lines of scrollback kept for the command's terminal in the TUI
timeout integer / string the group's, or no limit Time limit in fnug check and MCP runs (see below)
exclusive bool the group's, or false Never run alongside another command in parallel runs (see below)

timeout is whole seconds or a duration with units, such as 90s, 5m or 1h 30m. A command that runs longer in fnug check or an MCP run gets SIGTERM, then SIGKILL 3 s later, and is reported as TIMEOUT. The signals reach every process in the command's process group, unless the command streams to fnug's terminal: then only its own process gets them (see Commands without a terminal). 0 means no limit, overriding an inherited value and fnug check --timeout. There is no limit by default, and the TUI ignores timeout.

exclusive: true suits commands that rewrite files, such as formatters, so that nothing reads the files while they change. It matters only when fnug check --jobs or the MCP jobs parameter runs several commands at once; the TUI ignores it.

Auto fields

Field Type Default Description
git bool false Select when a file under path that matches regex has changed in git
watch bool false Select when a watched file under path that matches regex changes
always bool false Always select, regardless of changes
path list of strings the working directory Path prefixes that changed files must be under (e.g. "./src"); they may not exist yet
regex list of strings any file Patterns for the file's path relative to cwd (e.g. "^src/.*\\.rs$")
check bool true Set false to skip in fnug check, git hooks and MCP runs
run_on_change bool false In the TUI, run the command when a watched change selects it; needs watch

Which changes git looks at depends on the mode: uncommitted ones in the TUI and plain fnug check, and in fnug check the staged ones with --staged or those since a merge base with --base (see What fnug check runs). watch works while the TUI runs, and run_on_change is described under File watching. path: [] resets an inherited path to the command's own directory, and regex: [] clears an inherited regex.

A changed file selects a command when it is under one of its path entries and matches one of its regex patterns (any file, if there are none). The patterns see the file's path relative to the command's cwd, such as src/main.rs, or ../shared/lib.rs for a file outside it. Anchor with ^ to match from the cwd (^tests/), or write (^|/)tests/ to match a directory at any depth.

Neither git nor watch counts files that git ignores (through .gitignore, .git/info/exclude or core.excludesFile) or anything inside a .git directory. The watcher makes one exception: below a path that git ignores itself, such as ./target/doc, every change counts. It goes by the ignore rules alone, so it also skips a file that git tracks although a rule matches it, which git still counts. A watch path that doesn't exist when fnug starts is not watched. Like git, the watcher doesn't follow symlinked directories. On Linux, a directory that a .gitignore edit stops ignoring is only watched once fnug restarts.

Workspace fields

workspace: true walks the directory tree for package configs, down to the default max_depth. An object sets these instead:

Field Type Default Description
paths list of strings none: walk the tree Glob patterns for package directories, relative to the config
max_depth integer 5 How many directory levels the walk descends; unused with paths

Changelog

See CHANGELOG.md for what changed in each release, including breaking changes and what to do about them.

Metadata

Release files for fnug 0.1.0a14

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fnug 0.1.0a14
File Size Uploaded
fnug-0.1.0a14.tar.gz 441.4 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for fnug 0.1.0a14
File
fnug-0.1.0a14-py3-none-musllinux_1_2_x86_64.whl Python 3 none Linux musl 1.2+ x86-64 Details
fnug-0.1.0a14-py3-none-musllinux_1_2_aarch64.whl Python 3 none Linux musl 1.2+ ARM64 Details
fnug-0.1.0a14-py3-none-manylinux_2_28_x86_64.whl Python 3 none Linux glibc 2.28+ x86-64 Details
fnug-0.1.0a14-py3-none-manylinux_2_28_aarch64.whl Python 3 none Linux glibc 2.28+ ARM64 Details
fnug-0.1.0a14-py3-none-macosx_11_0_arm64.whl Python 3 none macOS 11.0+ ARM64 Details
fnug-0.1.0a14-py3-none-macosx_10_12_x86_64.whl Python 3 none macOS 10.12+ x86-64 Details

Total release size: 25.5 MB

Release files / fnug-0.1.0a14.tar.gz

Download URL fnug-0.1.0a14.tar.gz
Size 441.4 kB
Tags Source
SHA-256 checksum
How to use checksums
564bf78cbc3085f16d42192272566900526bff9d1ca25edea27967a7ba5c7a7d
BLAKE2b-256 checksum
How to use checksums
a1a7153a947939a59e5165ca8caa8495afd1e1c6626f0bafab40ec81d5a0e1ab
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.15.0

Release files / fnug-0.1.0a14-py3-none-musllinux_1_2_x86_64.whl

Download URL fnug-0.1.0a14-py3-none-musllinux_1_2_x86_64.whl
Size 4.5 MB
Tags Linux musl 1.2+ x86-64 Python 3
SHA-256 checksum
How to use checksums
663f27a1bf9061ed654930b103e38cce0f4367b945640146b831a3366799408b
BLAKE2b-256 checksum
How to use checksums
884d185ddf99ea227f903294f9759690467184c1df19bbf58029f01851bff838
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.15.0

Release files / fnug-0.1.0a14-py3-none-musllinux_1_2_aarch64.whl

Download URL fnug-0.1.0a14-py3-none-musllinux_1_2_aarch64.whl
Size 4.1 MB
Tags Linux musl 1.2+ ARM64 Python 3
SHA-256 checksum
How to use checksums
ca458d200bf0cae434b0c980adf40b0982441350500df020bc9756e2f1b79a6a
BLAKE2b-256 checksum
How to use checksums
bd8775ebcf0906a3c8f0441e873e518366b68b0f753d2ef97ff7f2ef6c90b425
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.15.0

Release files / fnug-0.1.0a14-py3-none-manylinux_2_28_x86_64.whl

Download URL fnug-0.1.0a14-py3-none-manylinux_2_28_x86_64.whl
Size 4.3 MB
Tags Linux glibc 2.28+ x86-64 Python 3
SHA-256 checksum
How to use checksums
9469994ece677688cee2c1aacd40872172d7faca211cbe59a31674b38e3dfb32
BLAKE2b-256 checksum
How to use checksums
93dd929b41cd689848943bc6e3cd6ec6f37ed2e03eb37bc8d215e6769344b398
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.15.0

Release files / fnug-0.1.0a14-py3-none-manylinux_2_28_aarch64.whl

Download URL fnug-0.1.0a14-py3-none-manylinux_2_28_aarch64.whl
Size 4.1 MB
Tags Linux glibc 2.28+ ARM64 Python 3
SHA-256 checksum
How to use checksums
b1ddcf34070188a2863ff21b73f3d530e43a7e7c7b30e0351c01e2d184baf316
BLAKE2b-256 checksum
How to use checksums
2d4d40d11bd12f3f6cd9d2509263b3137f4ff536ff435f086b401f8f1b3f27c4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.15.0

Release files / fnug-0.1.0a14-py3-none-macosx_11_0_arm64.whl

Download URL fnug-0.1.0a14-py3-none-macosx_11_0_arm64.whl
Size 3.9 MB
Tags Python 3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
8dce4f2da91e0aed2bee04c92717448d2318b60a63c7ea4b2f6d4087fc5c7a87
BLAKE2b-256 checksum
How to use checksums
a2d6fe922388dbce65a94555f735c430e1e0b5e9ff28f181e188d0a88468b0ed
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.15.0

Release files / fnug-0.1.0a14-py3-none-macosx_10_12_x86_64.whl

Download URL fnug-0.1.0a14-py3-none-macosx_10_12_x86_64.whl
Size 4.1 MB
Tags Python 3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
5503e5529ce94603da6045b9f904e03a2438a481001a936c9476b74c7d5aea0d
BLAKE2b-256 checksum
How to use checksums
e0fe9ea66dd5901a88fa104b2ff8058970965c1dbe1a1b881188dd18695ad66c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.15.0

Release history Release notifications | RSS feed

This release

0.1.0a14 This release

7 release files

0.0.19

2 release files

0.0.18

2 release files

0.0.17

2 release files

0.0.16

2 release files

0.0.15

2 release files

0.0.14

2 release files

0.0.13

2 release files

0.0.12

2 release files

0.0.11

2 release files

0.0.10

2 release files

0.0.9

2 release files

0.0.8

2 release files

0.0.7

2 release files

0.0.6

2 release files

0.0.5

1 release file

0.0.4

2 release files

0.0.3

2 release files

0.0.2

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page