Getting Started
Why this exists
Every registry has its own idea of a package document. npm keeps everything in one JSON with dist-tags, crates.io splits crate, versions and dependencies across three endpoints, PyPI has a JSON API for metadata and a Simple API for files, RubyGems answers versions and owners separately, Packagist nests versions under a vendor, and Arch Linux is two registries in one PURL type. Pulling one client per registry into a project means six response shapes and six ways to misread a license.
@agntn/registries puts six registries behind one class shape and one addressing scheme, the package URL of ECMA-427. Same calls, same Package, Version, Dependency and Maintainer types everywhere. The adapter decides how to ask; you decide what to ask for.
Install
pnpm add @agntn/registries
The AI SDK tool on the /ai subpath needs ai and zod next to it:
pnpm add ai zod
First lookup
import { fetchPackageFromPURL } from "@agntn/registries";
const pkg = await fetchPackageFromPURL("pkg:npm/lodash");
pkg.name; // "lodash"
pkg.latestVersion; // "4.18.1"
pkg.licenses; // "MIT", normalized to SPDX
pkg.repository; // "https://github.com/lodash/lodash"
Importing @agntn/registries registers the six built-in adapters as a side effect. From then on any PURL whose type is npm, cargo, pypi, gem, composer or alpm resolves through create() without a switch statement anywhere.
Same call, any registry
await fetchPackageFromPURL("pkg:cargo/serde");
await fetchPackageFromPURL("pkg:pypi/flask");
await fetchPackageFromPURL("pkg:gem/rails");
await fetchPackageFromPURL("pkg:composer/laravel/framework");
await fetchPackageFromPURL("pkg:alpm/aur/paru");
The Lookup explorer runs exactly these calls against the docs worker.
What ships
| Ecosystem | PURL type | Class | Registry |
|---|---|---|---|
| npm | pkg:npm/… | NpmRegistry | registry.npmjs.org |
| Cargo | pkg:cargo/… | CargoRegistry | crates.io |
| PyPI | pkg:pypi/… | PyPIRegistry | pypi.org |
| RubyGems | pkg:gem/… | RubyGemsRegistry | rubygems.org |
| Packagist | pkg:composer/… | PackagistRegistry | packagist.org |
| Arch Linux | pkg:alpm/… | AlpmRegistry | archlinux.org, aur.archlinux.org |
Each registry has its own page under Registries with the endpoints, the name rules and the traps.
The four lookups
Every adapter answers the same four questions. Lookups covers each in detail.
import {
fetchPackageFromPURL,
fetchVersionsFromPURL,
fetchDependenciesFromPURL,
fetchMaintainersFromPURL,
} from "@agntn/registries";
const pkg = await fetchPackageFromPURL("pkg:npm/lodash");
const versions = await fetchVersionsFromPURL("pkg:cargo/serde");
const deps = await fetchDependenciesFromPURL("pkg:pypi/flask@3.1.1"); // a version is required
const maintainers = await fetchMaintainersFromPURL("pkg:gem/rails");
A package that does not exist throws NotFoundError; a PURL that cannot be parsed throws InvalidPURLError; a type nobody registered throws UnknownEcosystemError. All of them extend PkioError, so one instanceof catches the lot.
One package shape
interface Package {
name: string;
description: string;
homepage: string;
documentation: string; // docs.rs, readthedocs, rubydoc, or empty
repository: string; // canonical https URL
licenses: string; // SPDX expression
keywords: string[];
namespace: string; // "@vue" on npm, "laravel" on Packagist, "arch" or "aur" on Arch
latestVersion: string;
metadata: Record<string, unknown>; // whatever else the registry knew
}
metadata is the escape hatch: crates.io download counts, AUR votes, Arch repo and architecture. Everything above it means the same thing on every registry.