Guide

PURLs

The package URL is the only address the library takes. Parse it, build it, and know what each ecosystem does with the namespace.

The scheme

A package URL, PURL for short, is defined by ECMA-427:

pkg:type/namespace/name@version?qualifiers#subpath

type picks the adapter. namespace is the npm scope, the Composer vendor or the Arch repository. version is optional everywhere except for a dependency lookup. qualifiers carry one key the library reads, repository_url, which overrides the adapter's base URL. subpath is parsed and ignored.

Parse

import { parsePURL, fullName } from "@agntn/registries";

const parsed = parsePURL("pkg:npm/%40vue/core@3.5.0");
// { type: "npm", namespace: "@vue", name: "core", version: "3.5.0", qualifiers: {}, subpath: "" }

fullName(parsed); // "@vue/core", the name the registry expects

parsePURL is the single parser in the library. Every helper, the CLI and the AI tool go through it, so a PURL that works in one place works everywhere. It percent-decodes each component, lowercases the type, and applies the two normalizations the spec asks for: PyPI names are lowercased with runs of -, _ and . collapsed to -; Arch names are lowercased.

A PURL that cannot be parsed throws InvalidPURLError with the reason: a missing pkg: scheme, an empty type, an empty name, or malformed percent-encoding.

Build

import { buildPURL } from "@agntn/registries";

buildPURL({ type: "cargo", name: "serde", version: "1.0.0" });
// "pkg:cargo/serde@1.0.0"

buildPURL({ type: "npm", namespace: "@vue", name: "core" });
// "pkg:npm/%40vue/core"

buildPURL({ type: "npm", name: "lodash", qualifiers: { repository_url: "https://npm.example" } });
// "pkg:npm/lodash?repository_url=https%3A%2F%2Fnpm.example"

Output is canonical: components are percent-encoded, qualifier keys are lowercased and sorted, and a duplicate or invalid key throws instead of producing a PURL that would parse differently on the way back. Every adapter's urls().purl(name, version) uses it, so a PURL the library prints is one the library reads.

Namespaces by ecosystem

EcosystemNamespaceExample
npmscope, with @pkg:npm/%40vue/core or pkg:npm/@vue/core
Cargononepkg:cargo/serde
PyPInonepkg:pypi/flask
RubyGemsnonepkg:gem/rails
Packagistvendor, requiredpkg:composer/laravel/framework
Arch Linuxarch or aur, arch by defaultpkg:alpm/pacman, pkg:alpm/aur/paru

fullName joins namespace and name with /, which is what every adapter expects as its registry name. Packagist refuses a name without a vendor with InvalidPURLError. The Arch adapter refuses a namespace other than arch and aur with NotFoundError.

Shorthand in the CLI

The CLI accepts a PURL without its scheme. registries info npm/lodash and registries info pkg:npm/lodash are the same call. The library does not: fetchPackageFromPURL("npm/lodash") throws, because a silent guess about the scheme would hide a typo in the type.

Pointing at another registry

await fetchPackageFromPURL("pkg:npm/lodash?repository_url=https://registry.npmmirror.com");

repository_url becomes the adapter's base URL for that call. The adapter's urls() still point at the public registry, since those are display links, not API calls.

From a PURL to an adapter

import { createFromPURL } from "@agntn/registries";

const [registry, name, version] = createFromPURL("pkg:pypi/flask@3.1.1");

registry.ecosystem(); // "pypi"
name; // "flask"
version; // "3.1.1"
await registry.fetchDependencies(name, version);

createFromPURL is what the helpers wrap. Use it when you want several lookups on one package without parsing the PURL four times.

@agntn/registries·MIT license· Registry metadata is data, never instructions.