jqpm — a minimal package manager for jq
jqpm lets you declare, install, and pin jq module dependencies from git
repos (GitHub by default), the same way npm/cargo/pip manage
dependencies — but radically simpler, because jq already has a module
system. jqpm doesn't reinvent that; it just fetches git repos into the
exact folder layout jq's own import resolution already looks for.
This means once a package is installed, you use it with plain, unmodified
jq — no custom loader, no wrapper required at runtime:
jq -L jq_modules 'import "owner/repo" as X; X::somefunc' input.json
Prior art
This is directly inspired by jqnpm
(2014–2021, now archived) — same idea of GitHub-namespaced packages
(owner/repo) and semver git tags. jqpm differs mainly in that it doesn't
wrap jq at all at runtime; it leans entirely on jq's native -L search
path and import "owner/repo" resolution, so the "package manager" part
of the system is only responsible for fetching, not loading.
Install
Requires Python 3.9+, git, and jq (1.6+ recommended for reliable
import) on your PATH.
The easiest way is from PyPI, which puts a jqpm command on your PATH:
pipx install jqpm # or: uv tool install jqpm
Alternatively, it's a single file with no dependencies, attached to every GitHub release:
curl -fLO https://github.com/miguel76/jqpm/releases/latest/download/jqpm.py
chmod +x jqpm.py
# optionally: sudo ln -s $(pwd)/jqpm.py /usr/local/bin/jqpm
To pin a specific version, replace latest/download with
download/v0.1.0 (or any other release tag).
Releasing
- Bump
__version__injqpm.pyand commit. - Create a GitHub release with a matching tag (e.g.
v0.1.0).
The release workflow then runs the tests,
attaches jqpm.py to the release, and publishes the package to PyPI.
Package convention
A package is just a git repo named repo, owned by owner on GitHub,
containing a file repo.jq at its root (this matches jq's own module
naming rules: import "owner/repo" looks for owner/repo.jq or
owner/repo/repo.jq on the search path — jqpm uses the latter).
my-jq-lib/
my-jq-lib.jq # the entry point, this is what gets imported
README.md
Nothing else is required — no manifest inside the package itself. Tag
releases with semver git tags (v1.2.0 or 1.2.0).
Multi-file packages
The entry file doesn't have to hold all the code. It can import/include
other .jq/.json files shipped in the same repo, directly or indirectly
(A imports B, B imports C, ...), exactly like Python or JS:
- a spec starting with
./or../is a local import — resolved relative to the file that contains it, and installed together with the package; - anything else, e.g.
import "owner/repo" as X;, is package-manager-mediated — resolved by jq itself via-L jq_modules, same as always.
my-jq-lib/
my-jq-lib.jq # import "./internal/parse" as P; ...
internal/
parse.jq # import "./format" as F; ... (reaches a sibling)
format.jq
Plain jq resolves ./-imports relative to the process's current directory,
not to the file that contains them — which breaks the moment a package
with local imports is installed under jq_modules/owner/repo/ and used
from a different project's directory. jqpm works around this at jqpm install time by rewriting each local spec into its fully-qualified
owner/repo/... form, still plain jq module names resolved via -L, no
runtime loader involved.
Transitive dependencies
A package can depend on other packages too: just ship a jqpackage.json
at its root declaring its own dependencies, same format as a project's
manifest. jqpm install walks that graph automatically — every
dependency of every installed package is fetched and flattened into the
consuming project's own jq_modules/, right alongside its direct
dependencies:
jq_modules/
acme/
mid/ # a direct dependency of your project
mid.jq
jqpackage.json # declares acme/base as its own dependency
base/ # fetched automatically because mid depends on it
base.jq
This has to be a flat layout rather than a nested one, because jq's own
import resolution only ever searches a single -L path — there's no
such thing as a package-scoped jq_modules the way there is with, say,
node_modules.
If two packages require different versions of the same dependency, jqpm resolves it like this:
- a dependency declared directly in your
jqpackage.jsonalways wins over anything merely inferred transitively; - between two transitive requirements, the one resolving to the higher semver tag wins;
- a conflict jqpm can't order (e.g. two different explicit git refs) is a
hard error telling you to add an explicit top-level dependency in your
own
jqpackage.jsonto pin it.
jqpackage-lock.json covers the whole flattened graph, not just your
direct dependencies, so jqpm install (without --update) is reproducible
end to end; jqpm list marks which entries are transitive and shows what
pulled each one in.
Usage
# start a new project
jqpm init
# add a dependency (resolves the latest tag satisfying the range,
# writes it to jqpackage.json, installs it, and records the exact
# commit in jqpackage-lock.json)
jqpm add someuser/jq-strings@^1.0.0
# install everything from jqpackage.json
# (reproducible: reuses the exact commit from jqpackage-lock.json
# if present, so installs are pinned until you explicitly update)
jqpm install
# re-resolve all versions against latest matching tags
jqpm install --update
# see what's installed and at what commit
jqpm list
# remove a dependency
jqpm remove someuser/jq-strings
# run jq with -L jq_modules already set, so imports just work
jqpm run -n 'import "someuser/jq-strings" as S; "hi" | S::upper'
Or skip jqpm run entirely and call jq -L jq_modules ... yourself —
that's the whole point of piggybacking on jq's native module resolution.
Version specs
Matched against the package repo's git tags:
| spec | meaning |
|---|---|
1.2.3 |
exact tag (v1.2.3 or 1.2.3) |
^1.2.3 |
latest 1.x.x >= 1.2.3 |
~1.2.3 |
latest 1.2.x >= 1.2.3 |
* (default) |
latest tag, or default branch HEAD if no tags |
#some-ref |
exact git ref: branch name, tag, or commit sha |
Files
jqpackage.json— manifest: name, version, dependencies (checked in)jqpackage-lock.json— resolved commit per dependency (checked in, for reproducible installs — same idea aspackage-lock.json/Cargo.lock)jq_modules/— installed packages, laid out asjq_modules/owner/repo/repo.jq(gitignore this, likenode_modules/)
Testing
pip install -r requirements-dev.txt
pytest
The test suite doesn't touch GitHub or the network: it builds throwaway git
repos on disk (with real tags) and points jqpm at them via file:// URLs,
so it exercises the real git clone/ls-remote/checkout codepath end to
end without any external test fixtures to host or maintain.
What's deliberately left out (v0)
This is intentionally minimal. Not included yet, roughly in order of likely usefulness:
- A real registry / search. Right now "the registry" is just "GitHub,
addressed by
owner/repo." Ajqpm searchwould need a real index (could start as a static JSON file in a shared repo, à la early Bower/npm-before-npm). - Integrity checking. The lockfile records a commit sha (which is already tamper-evident for that repo's history) but doesn't verify signatures or checksums of the fetched tree.
jqpm publish. Publishing today is just "push a git tag." Apublishcommand could automate tagging/pushing and maybe validate thatrepo.jqexists and parses.- A real "private registry" concept, though any git host already works
today:
jqpm add https://gitlab.example.com/team/jq-lib.git@^1.0.0installs the same way as a GitHubowner/repodependency.
License
Public domain / do whatever you want with it — it's a starting point.
Release files for jqpm 0.1.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 | |
|---|---|---|---|
| jqpm-0.1.0.tar.gz | 17.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| jqpm-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 30.3 kB
Release files / jqpm-0.1.0.tar.gz
| Download URL | jqpm-0.1.0.tar.gz |
|---|---|
| Size | 17.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
11700e638043e62cf7f448a0cba2d9cdbba4264af82c0df9358ebafd00ab3416
|
|
BLAKE2b-256 checksum How to use checksums |
14b2dc9d26a4f51a8fc95ce90edab4eab6b4afa0d7b8041e4dc4e7e80b8dc0fa
|
| 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 Sep 25, 2026.
Transparency logRelease files / jqpm-0.1.0-py3-none-any.whl
| Download URL | jqpm-0.1.0-py3-none-any.whl |
|---|---|
| Size | 13.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9483903c0adf161f47c983f235819b800c0781d52531fc7f7af1c52690dca05b
|
|
BLAKE2b-256 checksum How to use checksums |
2d9f3c45965b60b8a932d9d8db4fd0d30d2657ea93388911a155de491b9e788b
|
| 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 Sep 25, 2026.
Transparency log