nur
A script discovery and execution engine for your project's tasks.
Run nur in a project and it discovers the tasks your project already defines —
from npm, Make, deno, composer, just, Taskfile, pre-commit, PDM/poe, tox, mise,
cargo-make, moon, xc, VS Code tasks, nox, mask, Invoke, Rake, Grunt, Gulp,
Ant, Turborepo, Nx, and npm/pnpm/yarn/bun workspaces — then lets you run them from a TUI picker or directly from the command line.
Discovery is rooted at the current directory. See Features for the
full list of source files.
Table of Contents
- Installation
- Usage
- Support
- Motivation
- Features
- Author
- Analysis
- Contributing
- Development
- Releasing
- Credits
- License
- Changelog
Installation
nur is a standalone command-line tool — install it into an isolated
environment rather than as a project dependency:
uv tool install nur
Or run it without installing:
uvx nur
On macOS/Linux via Homebrew:
brew install hasansezertasan/tap/nur
On Windows via Scoop:
scoop bucket add hasansezertasan https://github.com/hasansezertasan/scoop-bucket
scoop install nur
To install the latest unreleased changes from source:
uv tool install git+https://github.com/hasansezertasan/nur
Or, from a clone:
uv tool install .
Verify the installation
The package root is importable after installation:
>>> import nur
>>> isinstance(nur.__version__, str)
True
Usage
Run nur from the root of a project that contains any supported task file.
TUI
Run with no arguments to open the interactive picker:
nur
Browse and run the discovered tasks in a three-pane Textual UI. Press q to
exit.
CLI
nur test # run a task by name (if unambiguous)
nur make:test # run a task by its qualified name
nur test -- --watch # pass extra args through to the underlying runner
nur list # print all discovered tasks
nur --version
Support ❤️
If you have any questions or need help, feel free to open an issue on the GitHub repository.
Motivation
Every project speaks a different task dialect — make test, npm run test,
just test, task test, pdm run test, poe test, tox -e test, mise run test, xc test. nur gives
you one command that discovers whatever a project already uses and runs it, with no config and
no need to remember which runner lives where. Discovery is pure text/JSON/TOML/XML
parsing, so listing tasks never executes anything (no make -pRrq side effects).
Features
-
Zero-config discovery across twenty-five providers, each parsed from a source file rooted at the current directory:
Provider Prefix Source file npm npmpackage.jsondeno denodeno.json/deno.jsonccomposer composercomposer.jsonMake makeMakefilePDM pdmpyproject.toml([tool.pdm.scripts])poe poepyproject.toml([tool.poe.tasks])tox toxtox.ini,setup.cfg,pyproject.toml, ortox.tomljust justjustfileTaskfile taskTaskfile.ymlpre-commit pre-commit.pre-commit-config.yamlmise misemise.toml(and variants — see below)cargo-make cargo-makeMakefile.tomlmoon moonmoon.ymlxc xcREADME.md(see below)VS Code vscode.vscode/tasks.json(see below)Invoke invoketasks.py(see below)nox noxnoxfile.py(see below)mask maskmaskfile.md(see below)Rake rakeRakefile(see below)Grunt gruntGruntfile.js(see below)Gulp gulpgulpfile.js(see below)Ant antbuild.xml(see below)Turborepo turboturbo.json(see below)Nx nxnx.json(see below)workspaces workspacemember package.jsonfiles (see below)makeparsesMakefilewith Tree-sitter without running Make. It lists literal targets from rule headers (including continued, multi-target,::and static-pattern rules) and an inline## description. Text indefineblocks, recipes and assignments is ignored; computed ($(VAR)), pattern and.-prefixed targets are skipped, andinclude,$(shell ...)and!=are never evaluated. Regions the parser rejects are read line by line with a warning, so the rest of the file is still listed. See ADR 0003.rakeparsesRakefilewith Tree-sitter without running Ruby orrake -T. UTF-8 and common ASCII-compatible Ruby source encoding declarations are supported; unsupported encodings are skipped with a warning. Non-ASCII task names in non-UTF-8 sources are omitted because their command arguments cannot be reproduced safely. Descriptions are decoded using the declared source encoding. It discovers direct literaltaskandmultitaskdeclarations, including dependency-only tasks, and tracks nested literalnamespaceblocks. Calls may use an explicitselfreceiver. The first line of an adjacent ordinary quoteddescsupplies optional description metadata. Comments preserve adjacency; other statements and namespace boundaries clear it. For example,task :migrateinsidenamespace :dbruns asrake db:migrateand is available asnur rake:db:migrate. Task bodies stay opaque, sodefinitionis empty. Conditional declarations, loops, classes, helper methods, callbacks,BEGINinitializers, dynamic names/namespaces, namespace block parameters and exception handlers, file tasks, rules, imported files andrakelib/*.rakeare skipped. Parentheses and plainbeginwrappers are transparent. Escaped/interpolated names, percent literals and heredocs are omitted. Task names must contain only letters, digits, underscores, colons, dots, slashes and hyphens, starting with a letter, digit or underscore. Names beginning with the reservedrake:lookup prefix and task names ending in a colon are skipped. Files with Ruby syntax errors reported by the parser are skipped with a warning. Single-entry task-name dependency hashes, including explicit braces, are supported; dependency expressions are not evaluated. Duplicate names are deduplicated, retaining the latest nonempty literal description. Multi-entry task-name hashes are omitted. These are syntactic task candidates, not a guarantee that Ruby can load the file or that Rake will register or successfully execute each task. Discovery does not simulate exceptions, control flow, method redefinitions or other Ruby behavior. Rake reports runtime failures when a task is invoked. See ADR 0002 for the support boundary.gruntparsesGruntfile.jswith Tree-sitter without running Grunt orgrunt --help. It discovers literalgrunt.registerTask/grunt.registerMultiTasknames (and theirgrunt.task.*aliases), including the default task, running each asgrunt <name>and available asnur grunt:<name>. The optional description argument supplies description metadata. Task bodies and alias lists stay opaque, sodefinitionis empty. Plugin-loaded tasks (loadNpmTasks), renamed tasks, external task files, computed names, andtask:targetspellings are skipped. These are syntactic task candidates, not a guarantee that Grunt will register or successfully execute each task.gulpparsesgulpfile.js(orGulpfile.js,gulpfile.mjs,gulpfile.cjs— the first present wins) with Tree-sitter without running Gulp orgulp --tasks. It discovers literalgulp.task('name', ...)/task('name', ...)names, CommonJSexports.name/module.exports.name/exports['name']assignments, and ESMexport function/export const/export default/export {a as b}forms, running each asgulp <name>.<task>.descriptionstring assignments supply description metadata. Task bodies, function references (task(build)),series()/parallel()composition, computed keys, object-literalmodule.exports = {...}, and re-exports are skipped. These are syntactic task candidates, not a guarantee that Gulp will register or successfully execute each task.antparses the declarativebuild.xmlwithout running Ant orant -p. It discovers every<target name="...">(including ones without adescription, whichant -phides as internal), running each asant <name>and available asnur ant:<name>. Target-like<extension-point name="...">entries are listed the same way. Names are kept verbatim (Ant permits spaces) except leading-hyphen targets, which Ant parses as options and can never run from the command line. Thedescriptionattribute supplies description metadata. Task bodies (XML tasks, not shell commands) stay opaque, sodefinitionis empty. Imported/included build files,macrodef-defined behaviour,if/unlessconditions,dependsordering, and${property}interpolation are not resolved; names are left literal. These are syntactic task candidates, not a guarantee that Ant will accept or successfully execute each target.turboreads the rootturbo.json(JSONC)taskstable (falling back to the legacy v1pipelinekey) and runs each key asturbo run <name>, orchestrating the matchingpackage.jsonscripts across workspace members. Root tasks (//#<name>) are listed under their stripped name, keeping the full//#form (run asturbo run //#<name>) only when the stripped name collides with another task. The file holds orchestration config rather than commands, sodefinitionis empty. Package-levelturbo.jsonfiles stay opaque.nxreads the rootnx.json(JSONC)targetDefaultskeys — the tasks shared across projects — and runs each asnx run-many -t <name>. Only literal target names surface: executor keys (@nx/js:tsc) and globs (e2e-ci--**/**) cannot run viarun-many -tand are skipped, while array-form filtered defaults are accepted like plain objects. Per-project-only targets (inproject.jsonfiles or inferred by plugins) stay opaque. These are syntactic task candidates, not a promise that every project implements each target.workspacediscovers memberpackage.jsonscripts in monorepos: globs come frompnpm-workspace.yaml(packages),package.json(workspaces), andlerna.json(packages, defaulting topackages/*unlessuseWorkspacesdefers topackage.json). Matching stays confined to the current directory (absolute patterns, escapes, andnode_modulesare never members), and only directories with their ownpackage.jsonqualify. Recursive (**) globs prunenode_modulesand.gitduring traversal instead of walking them. Each script is named<package>:<script>(the packagename, or its path) and runs through the lockfile-resolved package manager scoped to the member —pnpm --filter,yarn workspace,npm --workspace,bun run --filter, or, for lockfile-less Lerna projects,lerna run --scope.See ADR 0004 for the evaluation behind the JS-adjacent providers.
toxreads the first applicable config file present, in priority order:tox.ini,setup.cfg([tox:tox]),pyproject.toml([tool.tox]), thentox.toml. It statically parses environments and never runstoxduring discovery.misereads the first config file present, in priority order:mise.local.toml,mise.toml,.mise.local.toml,.mise.toml,.config/mise.toml.xcreads its task section fromREADME.md— the block marked with an<!-- xc-heading -->comment, or failing that a heading namedTasks(which is how nur discovers the tasks documented in this very file).composerreads the top-levelscriptsobject fromcomposer.json, surfacing only user-defined custom scripts (Composer's reserved lifecycle hooks likepost-install-cmdare filtered out) and taking descriptions from thescripts-descriptionstable when present.pre-commitreads onlyrepos[].hooks[]and runs a selected hook withpre-commit run <id> --all-files; it never invokespre-commitduring discovery.vscodereads version2.0.0.vscode/tasks.json(JSONC) and surfacesshellandprocesstasks with acommand(an untyped task runs as a process, as in VS Code), applying the current platform's overrides, inheriting the document-leveltypeand, for tasks without their owncommand, itscommandandargs, and resolving${workspaceFolder},${workspaceFolderBasename},${pathSeparator}, and${env:NAME}. Tasks nur cannot run faithfully are skipped: hidden tasks, tasks with a non-emptydependsOn, other variables, oroptionssettingenv,shell, or acwdother than the project root. Shell tasks run through$SHELL -con POSIX (falling back to/bin/sh) andcmd.exeon Windows; an object-formcommandis quoted as a single literal token.invokeparsestasks.pyas a Python syntax tree without importing it or runninginv --list. It discovers top-level functions with a single@task,@task(...), or@invoke.taskdecorator, including import aliases. Literalname=and list/tuplealiases=are supported; underscores become dashes following Invoke's default configuration. Tasks run asinvoke <name>with extra CLI flags passed through, and the first docstring line is the description. Command bodies, required parameters, pre/post hooks, imported or dynamic tasks, conditional definitions, custom decorators, and explicitCollectionwiring are not resolved. Explicit namespaces may therefore make the discovered bare names unavailable, and configuration disabling automatic dashes is not read. Names and aliases containing dots or beginning with a dash are excluded because Invoke interprets them as namespace paths or CLI options.noxparsesnoxfile.pyas a Python syntax tree (never importing it, unlikenox --list) and surfaces top-level@nox.session-decorated functions, runnable asnox -s <name>, using an explicit stringname=when given and the docstring's first line as the description.nox-uv's drop-insessiondecorator is recognised too, andpython=[...]/@nox.parametrizevariants appear under their base name (which runs every variant). Only straight-line noxfiles are trusted: module code may use imports, assignments,def/class, andif/tryblocks made of those (such as try-import fallbacks andif sys.version_info < ...: raiseguards);if TYPE_CHECKING:andif __name__ == "__main__":blocks are skipped. A noxfile that uses loops,with,match,assert,raiseinside atry, or that editsnox.sessionor a namespace directly lists no sessions, rather than sessions nox may not register. Discovery is best-effort: it cannot know whether an import or an expression fails in nox's environment, and deliberately unusual module code (aliasing tricks, handler matching insidetry, and similar) may still list a session nox rejects. Running such a session fails with nox's own error; nur never runs noxfile code to discover sessions.maskreadsmaskfile.mdthe way mask does: each heading after the title is a command, a deeper heading is its subcommand, the last fenced block under a heading is its script, and a>blockquote is its description. A subcommand is named by its path and runs with it, so### test lintunder## testis the tasktest lint(nur "test lint"runsmask test lint). Task names are the shell words aftermask, so one command whose own name has a space is quoted:## deploy prodis the task'deploy prod'. A heading becomes a task only when its last code block is fenced with a language tag, since mask needs the tag to pick an interpreter;powershell,batch, andcmdblocks count only on Windows, as in mask. Headings and fences inside HTML blocks (comments,<details>, and the like) are ignored. Fences nested in blockquotes are not recognised. Like mask, a heading's inline markup restarts its name, so## a *b* cis the taskb c. -
CLI Application: run any discovered task by name or qualified
prefix:name, with--passthrough to the underlying runner. -
TUI Application: interactive three-pane task picker built with Textual.
-
Safe by default: discovery parses files; it never shells out to a runner just to list tasks.
-
Type Safety: full type hints checked by mypy, basedpyright, ty, pyrefly, and zuban.
-
Code Quality: comprehensive linting and formatting with ruff, plus architecture-contract enforcement with import-linter.
-
Testing: pytest with coverage reporting and parallel execution.
-
Documentation: Sphinx documentation with the Shibuya theme, GitHub Pages deployment, and live per-PR documentation previews.
-
CI/CD: automated testing, building, and publishing across multiple platforms.
-
Security: CodeQL, OpenSSF Scorecard, dependency review, secret scanning (gitleaks), dependency auditing (pip-audit), GitHub Actions static analysis (zizmor — a blocking prek/CI gate plus a Security-tab dashboard, over hardened least-privilege workflows), and a CycloneDX SBOM attached to every release.
-
Managed
.gitignore: kept in sync with the upstream github/gitignore templates by cobo, with a weekly drift check. -
Modern Python: uv for dependency management, hatch for building.
Author 🫅
This project is maintained by Hasan Sezer Taşan, It's me 👋
Analysis
- Snyk Python Package Health Analysis
- Libraries.io - PyPI
- Safety DB
- PePy Download Stats
- PyPI Download Stats
- Pip Trends Download Stats
- PyPI Map Dependency Graph
Contributing ❤️
Any contributions are welcome! Please follow the Contributing Guidelines to contribute to this project.
Development 🧰
Clone the repository and cd into the project directory:
git clone https://github.com/hasansezertasan/nur
cd nur
install
Install the dependencies:
uv sync
style
Run the style checks:
uv run --locked tox run -e style
ci
Run the CI pipeline:
uv run --locked tox run
docs-build
Build the documentation site:
uv run --locked tox run -e docs-build
docs-server
Start the live-reloading docs server:
uv run --locked tox run -e docs-server
docs-linkcheck
Check the documentation for broken links (also runs weekly in CI):
uv run --locked tox run -e docs-linkcheck
Releasing
Versioning and releases are automated with release-please, driven by Conventional Commit PR titles squash-merged into main. release-please maintains a release PR that bumps the version and CHANGELOG.md; merging it tags the release and publishes to PyPI. See the Contributing Guidelines for the commit conventions, and the Repository setup guide for one-time configuration and optional post-launch integrations such as a social preview, downstream packaging, and Repology.
Pause merges that change .github/workflows/, including Renovate action-pin updates, from merging a release PR until the entire Release run finishes. Workflow differences between the release commit and current main can block tag or release creation with a 403 when using GITHUB_TOKEN. See Release recovery for prevention and recovery steps.
For adoption or template-update reconciliation, ask your agent to "audit this
template adoption/update against our existing project behavior".
The shipped template-adoption skill
also supports audits after an update was applied. It compares workflow behavior,
required checks, custom tooling, and documentation, and asks before consequential
cleanup. Its prek-workflow example shows why duplicate commands alone do not
justify removing a workflow. Use the sibling repo-setup skill for repository
settings and release setup.
Credits
This package was created with Copier and the hasansezertasan/copier-pyproject project template.
License 📜
This project is licensed under the MIT License.
Changelog 📝
For a detailed list of changes, see the GitHub Releases. A CHANGELOG.md is generated automatically by release-please on each release.
Metadata
Release files for nur 0.10.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| nur-0.10.0.tar.gz | 206.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| nur-0.10.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 339.8 kB
Release files / nur-0.10.0.tar.gz
| Download URL | nur-0.10.0.tar.gz |
|---|---|
| Size | 206.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
608018eb43beca190ad80473576416ccdc36e0009c66cceb475b9db9128ce18b
|
|
BLAKE2b-256 checksum How to use checksums |
dd514a57760a738abf039d41671596ec0b1f1894b10cd2a713d2ba9ccf6896f7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.
Transparency logRelease files / nur-0.10.0-py3-none-any.whl
| Download URL | nur-0.10.0-py3-none-any.whl |
|---|---|
| Size | 133.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
97fbecd87ed4801f21c3db73eae296d9e83cc29c42e15384a8f1732498ca75f8
|
|
BLAKE2b-256 checksum How to use checksums |
a28b86ccf65a8c7bbb283cab1cac2f4395357cb403d06ec526a3bfcc99265dd8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.
Transparency log