This release is a pre-release and may not be stable for production use.
Fnug
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.
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.yamlfor 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 runsfnug checkand add the MCP server to your editor - MCP server (
fnug mcp) — let coding agents list and run your checks - Command dependencies — define
depends_onto control execution order, and run independent commands in parallel withfnug 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.yamlfiles 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 -Nstay intent-to-add. - It works on the repository that contains the current directory, as
--stageddoes, 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 --stashruns, such as the pre-commit hook: the files the checkout and the restore change don't select commands or startauto.run_on_changeruns, a config reload (from a saved config file orF5) is put off, and the toolbar saysWaiting 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 --stashin 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, otherfnug checkruns 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 .andpytestonly ifpyproject.tomlconfigures or depends on them (ormypy.ini,pytest.iniorconftest.pyexists). The commands are prefixed withuv run,poetry runorpdm runwhen that tool's lockfile exists. - Node: the scripts run with pnpm, yarn, bun or npm to match the lockfile.
testgetsCI=trueso test runners don't start in watch mode, and is left out if it is the placeholdernpm initwrites. - Go: the gofmt check fails if gofmt fails or lists any file. Like
go vet ./..., it skipsvendor/,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()runsfnug check, with its options as keyword arguments:targets,all_,include_manual,base,staged,stash,fail_fast,no_tui,mute_success,jobs,timeoutandallow_modifications.fnug.start()opens the TUI.- Both take the global options
config_path,root,no_workspace,log_fileandlog_level, or aConfigas their first argument instead ofconfig_path. They return thesubprocess.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:
- an id, or the name of one of the command's siblings;
- a group path like
backend/test; - a name that is unique in the file;
- 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)
| File | Size | Uploaded | |
|---|---|---|---|
| fnug-0.1.0a14.tar.gz | 441.4 kB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| 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
|