Skip to content

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:

TypeConfig blockFieldsWhere the manifests live
local-dirlocal-dir:pathA directory on the host machine.
gitgit:remote, ref?A remote git repository
ocioci:refA 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 nem

The catalog directory layout must strictly follow these rules:

  • All package directories must be immediately under pkgs/. nem iterates pkgs/ 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 under pkgs/ with no pkg.yaml is silently ignored, and a pkg.yaml that fails to load or validate is skipped with a warning rather than breaking the whole catalog.
  • Directory name equals package identity. i.e. kubectl package must live in pkgs/kubectl/. The name: field in pkg.yaml must match the directory name exactly.
  • Extra content is allowed. The root may hold a README, a LICENSE, docs, CI config, or anything else alongside pkgs/; nem only reads pkgs/.
Package directories are flat — there is no nesting or namespacing under 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-catalog

Or 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.git

Or 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 main

Run 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.0

When 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:v1

Or 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.

This is the recommended way to distribute catalogs to consumers, and how 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-registry

Or add it with nem catalog add oci myteam ghcr.io/org/catalog:v1.

In which:

  • ref points directly at the catalog index.
  • Run nem catalog update to pull the latest catalog index and package manifests.
  • Pinning a digest (<base>@sha256:…) pins the entire metadata tree; nem catalog update is then a no-op.
  • artifacts selects 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>:&lt;tag&gt;<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+yaml carrying the raw pkg.yaml bytes
{
  "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 declared fetch, unchanged from a catalog with no artifacts field 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 a registry-mode catalog never contacts an upstream host at all.
  • prefer-registry — tries the registry first and falls back to the manifest’s declared fetch only 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 v1

nem 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.