Catalog
A catalog is a collection of package manifests that nem searches to discover and install tools.
Each package is described by a single pkg.yaml manifest, which declares the package’s metadata, dependencies, and how to fetch and install it.
Catalog types
A catalog is distributed to nem as one of three types, set by the type:
field of a catalog entry in config.yaml (see
Configuration). All three carry the same set
of pkg.yaml manifests and differ only in where those manifests live and how
nem fetches them:
| Type | Config block | Fields | Where the manifests live |
|---|---|---|---|
local-dir | local-dir: | path | A directory on the host machine. |
git | git: | remote, ref? | A remote git repository |
oci | oci: | ref | A remote OCI registry |
local-dir
The catalog is a directory on disk with a top-level pkgs/ directory that nem
reads in place. Edits to the catalog are immediately visible to nem.
my-catalog/ # the catalog root
├── pkgs/ # required; holds one directory per package
│ ├── kubectl/
│ │ └── pkg.yaml # name: kubectl (must match the directory)
│ ├── helm/
│ │ └── pkg.yaml # name: helm
│ └── ripgrep/
│ └── pkg.yaml # name: ripgrep
├── README.md # optional; ignored by nem
└── LICENSE # optional; ignored by nemThe catalog directory layout must strictly follow these rules:
- All package directories must be immediately under
pkgs/.nemiteratespkgs/and reads every subdirectory as a package candidate. Nested directories are ignored. - One manifest per package. Every package directory must contain a
pkg.yaml. A directory underpkgs/with nopkg.yamlis silently ignored, and apkg.yamlthat fails to load or validate is skipped with a warning rather than breaking the whole catalog. - Directory name equals package identity. i.e.
kubectlpackage must live inpkgs/kubectl/. Thename:field inpkg.yamlmust match the directory name exactly. - Extra content is allowed. The root may hold a
README, aLICENSE, docs, CI config, or anything else alongsidepkgs/;nemonly readspkgs/.
pkgs/.
Every package is pkgs/<name>/pkg.yaml, and <name> is unique within the
catalog. Cross-package relationships are expressed with
dependsOn, not directory
structure.Add a local-dir catalog by running:
# nem catalog add local-dir <name> <path>
nem catalog add local-dir myteam /path/to/my-catalogOr add it directly to config.yaml:
catalogs:
- name: myteam
type: local-dir
local-dir:
path: /path/to/my-catalog # the catalog root (holds pkgs/)Since edits to local-dir catalogs are immediately visible to nem, it’s the natural choice for local development of new packages.
git
The catalog is a git repository whose working tree uses the same layout as
local-dir.
nem clones the repository into $NEM_HOME/catalogs/<name>/ on first use and reads it in place like a local-dir catalog.
Add a git catalog by running:
# nem catalog add git <name> <remote> [--ref <branch|tag|commit>]
nem catalog add git myteam https://github.com/org/catalog.gitOr add it directly to config.yaml:
catalogs:
- name: myteam
type: git
git:
remote: https://github.com/org/catalog.git
ref: main # optional branch/tag/commit; defaults to mainRun nem catalog update to fast-forwards the clone to the latest commit.
nem refuses to update a git catalog if the local clone has uncommitted changes. Use git stash or git reset --hard to discard local changes before updating.oci
The catalog is packaged as OCI artifacts and hosted in an OCI registry. nem provides built-in utility to convert a local-dir catalog to oci catalog and publish it to a registry.
The registry layout for an oci catalog is as follows:
ghcr.io/org/catalog registry base
│
├── :v1 catalog index pointing to package manifests
│ ├── kubectl → pkg.yaml
│ ├── helm → pkg.yaml
│ └── ripgrep → pkg.yaml
│
├── archives/ release blobs
│ ├── kubectl:1.30.0
│ └── helm:3.15.0
│
└── sources/ source blobs
└── ripgrep:14.1.0When working with an oci catalog, nem pulls the catalog into a local OCI-layout store under $NEM_HOME/catalogs/<name>/store/.
To add an oci catalog, run:
# nem catalog add oci <name> <base>[:<tag>][@sha256:<digest>]
nem catalog add oci myteam ghcr.io/org/catalog:v1Or add it directly to config.yaml:
catalogs:
- name: myteam
type: oci
oci:
ref: ghcr.io/org/catalog:v1 # <base>[:<tag>][@sha256:<digest>]Run nem catalog update to pull the latest catalog index and package manifests into the local store.
nem’s official catalog is distributed.For more details on the OCI registry layout, see OCI Catalog Layout below.
OCI catalog layout
An oci catalog stores a whole catalog in an OCI registry as standard OCI
artifacts, so any OCI-compliant registry can host it.
The layout is a convention built from two parts:
- A metadata tree — an OCI image index referencing multiple OCI image manifests.
Each manifest carries a package’s
pkg.yaml. This is what identifies the catalog and every package in it. - Package payloads — the release archives and source blobs a package installs from, stored in sibling sub-repos.
Everything is content-addressed, so an entire catalog can be pinned by digest for exact reproducibility.
Consumer config
Add an oci catalog to config.yaml:
catalogs:
- name: myteam
type: oci
oci:
ref: ghcr.io/org/catalog:v1 # <base>[:<tag>][@sha256:<digest>]
artifacts: upstream # upstream (default) | registry | prefer-registryOr add it with nem catalog add oci myteam ghcr.io/org/catalog:v1.
In which:
refpoints directly at the catalog index.- Run
nem catalog updateto pull the latest catalog index and package manifests. - Pinning a digest (
<base>@sha256:…) pins the entire metadata tree;nem catalog updateis then a no-op. artifactsselects where package archives and sources download from; see Artifact source below.
Registry layout
An oci catalog is stored in an OCI registry with the following layout:
<base> registry base
├── :<tag> catalog index application/vnd.oci.image.index.v1+json
├── archives/<pkg>:<version> release archives application/vnd.oci.image.index.v1+json (multi-arch)
└── sources/<pkg>:<version> source blobs application/vnd.oci.image.manifest.v1+json (buildFromSource only)The catalog index and the per-package manifests it references form one content-addressed Merkle DAG. Every edge is a digest, so pinning the index digest transitively pins the whole metadata tree:
graph TD
IDX["<base>:<tag><br/>image index<br/>(schemaVersion annotation)"]
M1["title: kubectl<br/>artifactType: …nem.pkg.v1+yaml"]
M2["title: helm<br/>artifactType: …nem.pkg.v1+yaml"]
L1["layer blob<br/>kubectl/pkg.yaml"]
L2["layer blob<br/>helm/pkg.yaml"]
IDX -->|"manifests[] (by digest)"| M1
IDX -->|"manifests[] (by digest)"| M2
M1 -->|"layer (by digest)"| L1
M2 -->|"layer (by digest)"| L2
Archive and source blobs live in the sibling archives/ and sources/
sub-repos, outside this metadata DAG.
Catalog index
The catalog index is a standard OCI image index. It carries references to each package’s manifest in the manifests array. Each manifest has title and optional description annotations describing the package.
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"annotations": {
"org.vi-dev.nem.catalog.schemaVersion": "1"
},
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:<digest>",
"size": 512,
"annotations": {
"org.opencontainers.image.title": "kubectl",
"org.opencontainers.image.description": "Kubernetes CLI"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:<digest>",
"size": 312,
"annotations": {
"org.opencontainers.image.title": "helm"
}
}
]
}Package manifest
Each entry in the catalog index’s manifests array points to a package manifest, which is a standard OCI image manifest with:
artifactType: application/vnd.nem.pkg.v1+yaml- An empty config blob (
application/vnd.oci.empty.v1+json) - One layer of media type
application/vnd.nem.pkg.v1+yamlcarrying the rawpkg.yamlbytes
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"artifactType": "application/vnd.nem.pkg.v1+yaml",
"config": {
"mediaType": "application/vnd.oci.empty.v1+json",
"digest": "sha256:<digest>",
"size": 2
},
"layers": [
{
"mediaType": "application/vnd.nem.pkg.v1+yaml",
"digest": "sha256:<digest>",
"size": 512
}
]
}Artifact source
By default, an oci catalog’s package archives and sources still download
from wherever each pkg.yaml’s fetch block points — a GitHub release, or
any other HTTP host — exactly like a git or local-dir catalog. The
artifacts field on the oci: block (see Consumer config
above) redirects those downloads to the catalog’s own registry instead:
upstream— the default. Archives and sources download via each manifest’s declaredfetch, unchanged from a catalog with noartifactsfield at all.registry— strict: archives and sources come only from<base>/archives/<pkg>:<version>/<base>/sources/<pkg>:<version>. A missing repository, tag, or platform entry is a hard error. Resolving an unpinned version (nem use <pkg>,nem upgrade) lists the registry’s tags instead of contacting upstream, so aregistry-mode catalog never contacts an upstream host at all.prefer-registry— tries the registry first and falls back to the manifest’s declaredfetchonly when the artifact isn’t mirrored (missing repository, tag, or platform entry). Any other registry error — a digest mismatch, auth failure, or network failure — is reported immediately instead of falling back, so a corrupted or misconfigured mirror never silently degrades to upstream. Version resolution stays upstream in this mode.
NEM_OCI_ARTIFACTS=<mode> overrides the mode for every OCI catalog,
including the implicit official one, taking precedence over the
per-catalog artifacts field. An unrecognized value — in either the
environment variable or config.yaml — fails config loading with an error.
Populate the registry’s archives/ and sources/ repositories with
nem author mirror.
Publishing an oci catalog
Start with a local-dir catalog. Then run:
# nem author publish <registry-base> [catalog-dir] --tag <tag> [--tag <tag> ...]
nem author publish ghcr.io/org/catalog ./catalog --tag v1nem will validate the catalog, assemble the catalog index and package manifests, and push them to the registry. The --tag option is repeatable to push multiple tags for the same catalog index.
The resulting index digest is printed so you can pin it in consumer config:
Published ghcr.io/org/catalog@sha256:… (tags: v1)Manifests are assembled deterministically (no embedded build timestamp), so an
unchanged pkg.yaml keeps the same digest across publishes.
nem author publish only pushes catalog metadata — the pkg.yaml manifests
and the index referencing them. To also populate archives/ and sources/
with the package artifacts themselves, run
nem author mirror.