Registry
The lists you added. The source of truth. If Odyn cannot read it, it stops.
Odyn has no package registry. odyn search anima still hands you a list of Odin
packages, each ready to install. This page is about how that happens without any
server Odyn controls.
Most package managers own a central index. Someone runs it, packages publish to it, the tool queries it. That index is convenient, and it is also the thing that makes a package manager a gatekeeper.
Odyn keeps a registry of lists instead. A list is a git repository that happens to describe some packages. Odyn hosts none of them and vouches for none of them. It reads what you point it at.
your git URLs ──► registry of lists ──► ranked matches for a querySo the job of search is narrow: take the handful of URLs you added, and turn
them into a flat, ranked answer. Everything else is detail. Let us follow
odyn search anima.
Search touches two locations, and confusing them is the only real way to get lost.
Registry
The lists you added. The source of truth. If Odyn cannot read it, it stops.
Cache
Copies of those lists, kept for reading. Throwaway. Delete it and search rebuilds it.
The registry lives with your config; the cache lives with your other caches. The invariant behind everything: the cache is always rebuildable from the registry, and never the reverse.
A list is identified by its URL, but a URL makes a terrible directory name. So the cache names each list’s folder after a hash of that URL.
awesome-odin ─► https://github.com/jakubtomsu/awesome-odin │ hash ▼ ~/.cache/odyn/lists/de425e5483e8547d/The hash is never written down. It is a plain function of the URL, so search recomputes it every run and lands in the same place. Nothing to store, nothing to keep in sync.
The URL is the right thing to hash, and the alternatives are worse:
Since those folder names are unreadable, Odyn leaves a guide next to them,
INDEX.txt, mapping each hash back to a name and URL. Odyn never reads it. It is
there for a human poking around the cache.
To read a list, Odyn needs its repository, and it takes the smallest copy that works: shallow, with file contents deferred until something actually reads them. A few forges do not support that, so a plain shallow clone is the fallback.
not cached ──► thin clone ──► fails? ──► plain shallow clonecached ──► fetch latest ──► fails? ──► search the copy you already haveThat last arrow matters.
Throughout the fetch, search shows a single line for the whole batch, not one per list:
◜ Searching for 'anima' in 2 lists...This is a deliberate difference from sync, which reports every dependency it
restores. In sync, that report is the only place you learn what happened, so
it earns its noise. In search, the results are the report, so narrating each
fetch would only say the same thing twice.
A cloned list is a folder. Which file in it lists the packages?
Odyn_Index.toml present? │ yes ──► read the file it points to, in the format it names │ no ──► read README.md as a markdown tableOdyn_Index.toml holds no packages itself. It is a signpost: this file, in this
format. When it is missing, Odyn falls back to the README that already exists,
and this is where the design earns its keep.
However a list is read, it yields the same thing every time: a package with a name, a repo, a description, a license, and tags. Three parsers get there, and they all end at the same shape.
TOML ─┐ JSON ─┼─► a package { name, repo, description, license, tags } Markdown ─┘Structured and exact, chosen by an author who wants control. A real TOML parser reads it, so a malformed file fails loudly instead of being quietly misunderstood.
An author writes format = "toml" in the index and points it at a file of
[[package]] blocks.
The same structured road for authors who would rather write JSON. It carries the
same fields and the same arrays, so tags stays a real list rather than a
string someone has to split by hand.
An author writes format = "json" and points at a file shaped like
{ "packages": [ ... ] }.
Tolerant, for READMEs nobody wrote for Odyn. Odyn scans every table and keeps the ones shaped like package tables: a name column whose cells hold the repo as a link, and a description column. Several such tables in one document collapse into a single flat set.
The TOML and JSON roads are structured formats an author opts into; the Markdown road is the fallback that needs no opt-in at all. What matters is that none of them is special downstream. Search consumes a list of packages and never asks which parser produced it, so a new road is only ever a new producer of the same shape. That is exactly why adding the JSON road cost so little: one parser, and nothing else in search had to know.
Now the packages exist, and the query has to find its own among them. Odyn
compares anima against two fields: the display name and the repo slug, because
people search by both.
"anima" │ ├─ appears inside the target? ──► match, best score (substring) │ └─ close enough to it? ──► match, ranked lower (typo tolerance)The second tier only runs for queries of five characters or more. Below that, edit distance is meaningless: almost anything is two edits from a three-letter word, so a short query would drag in noise. Short queries stay exact.
Facets stack on top as filters that narrow, not rank:
odyn search anima --tag 2d --license MIT │ └─ name matches "anima" AND tagged 2d AND MIT licensedOnly the name produces a relevance score; the facets are gates. A package survives if it clears every active one. When there is no name at all, results fall back to alphabetical order.
The survivors are sorted, the spinner is wiped, and its line is replaced by the result block. Each line names the source list as a colored prefix, then the package’s install slug.
awesome-odin:asbott/jamginemylist:someone/anima (Codeberg)
help: to install a package, "get" without the list prefix and adjust the platform
odyn get asbott/jamgine odyn get someone/anima -p codeberg
note: found 2 results for 'anima' on 2 listsTwo quiet decisions live in that block.
A forge Odyn recognizes gets a label and the matching -p flag in its install
hint; one it does not gets its full URL, which is exactly what get accepts
anyway. The same knowledge drives the label and the flag, so what search shows
and what get wants can never disagree.
And the help block teaches, rather than lists. It shows the top result as the
base example, and adds a -p line only if some result actually needs one. Ten
results do not print ten commands; they print the one pattern you needed to see.
None of it touches a server Odyn owns, because there is none. There are only the lists you chose, read fresh each time.