Guide

promptregistry turns a static JSON manifest into typed, greppable named imports with a committed lockfile. The package is a TypeScript SDK + a small CLI built on top of promptkit.

It is variables only, no template logic — no {'{{#if}}'}, no loops, no DSL. If you need conditionals or iteration, build them in TypeScript and compose strings.

Installation

npm install @nkwib/promptregistry
# Optional: install typescript if you want to use `promptregistry init`.
npm install --save-dev typescript

Requires Node 20+. The CLI installs a single bin entry: promptregistry.

Author a manifest

The manifest is a JSON file you host anywhere a static GET will reach it — GitHub raw, a release asset, or a public bucket. Each entry has a name, a version tag, a template, and a delimiter pair.

{
  "manifest-format-version": "1",
  "prompts": [
    {
      "name": "customer-summary",
      "version": "v1",
      "template": "Summarize the account for {{customerName}} on the {{planTier}} plan. They joined on {{joinDate}}.",
      "delimiter": { "open": "{{", "close": "}}" }
    }
  ]
}

Names must match [A-Za-z0-9-_]+. Version is an opaque string — no semver inference, on purpose. Pin format is name@version.

Run codegen

npx promptregistry codegen --manifest ./manifest.json --out ./prompts/.generated

This emits, per prompt, one runtime <name>.ts whose default export is a CompiledTemplate<XxxVars>, plus a registry.ts barrel that re-exports each as a named identifier (kebab-cased to camelCase). It also writes prompt-lock.json next to the generated files, pinning every entry to its content hash.

The generated module shape is:

// prompts/.generated/customer-summary.ts (generated)
import { compile } from '@nkwib/promptregistry/runtime';

export type CustomerSummaryVars = {
  customerName: string;
  planTier: string;
  joinDate: string;
};

const _template = compile<CustomerSummaryVars>(
  'Summarize the account for {{customerName}} on the {{planTier}} plan. They joined on {{joinDate}}.',
  { open: '{{', close: '}}' },
);

export default _template;

The named XxxVars alias is the load-bearing trick: tsc's native missing-property error already names the prompt as 'CustomerSummaryVars', so the rewriter only needs to add the version-pin escape hatch.

Consume the barrel

import { customerSummary } from './prompts/.generated/registry.js';

const text = customerSummary.with({
  customerName: 'Ada',
  planTier: 'Pro',
  joinDate: '2024-01-15',
});

Under --module nodenext, the .js extension on the import is required even though the file on disk is .ts.

The exported customerSummary is a CompiledTemplate<CustomerSummaryVars>. It carries .with(vars), .partial(vars) (returns a new compiled template), and the placeholder array.

check + drift detection

npx promptregistry check --manifest ./manifest.json --out ./prompts/.generated

check cross-compares four things and exits non-zero on any disagreement:

CheckWhat it catches
hash-driftThe remote manifest was edited without a version bump.
stale-dtsA generated .ts header hash ≠ the lockfile hash (codegen wasn't re-run).
missing-pinA generated file references a Pin that has no lockfile entry.
orphaned-entryLockfile holds a Pin no longer present in the generated set (warning, not error).

Wire it into the build:

{
  "scripts": {
    "typecheck": "promptregistry check && tsc --noEmit"
  }
}

check --tsc

promptregistry check --tsc runs a regular tsc --noEmit after the basic checks pass, then rewrites any diagnostic that involves a generated XxxVars type. The locked wording is:

Remote prompt 'customer-summary@v1' removed variable 'joinDate' — update the call site (src/main.ts:5) or pin to a previous version.

Because the type alias name carries the prompt identity, the rewriter only fires for diagnostics that actually involve the generated surface — unrelated tsc errors pass through untouched.

Non-goals

  • No hosted UI, no backend, no auth.
  • No template logic — variables only.
  • No eval running. Use promptfoo for that.
  • No migration importers from Langfuse / PromptLayer.
  • pull('name@version') is internal-only (ADR-0004). Consumers always go through the barrel.
promptregistry Static prompt manifest, typed named imports, lockfile-gated integrity