opsinjs
HandbookRegistry & distribution

Registry & distribution

opsinjs is distributed as a shadcn-spec registry rather than as an npm package, and this is what that decision buys and what it costs.

Overview

opsinjs is not an npm package you depend on. It is a registry you copy from. You run a command, source files land in your repository, and from that moment you own them: you can read them, edit them, delete them, and nobody can change them underneath you.

That model comes from shadcn, and opsinjs implements shadcn's registry specification rather than inventing one. The decision is recorded as ADR 0002. Distribute as a shadcn registry. This page explains what the trade actually is, because it is a real trade with a real cost and the cost is usually described less clearly than the benefit.

All sixty ids carry real files with content. That content is the component itself plus the shared substrate, lib/opsinjs.ts and lib/status.ts. Every id resolves to installable source and each carries meta.opsinjs.implemented: true. A planned id would omit files and report implemented: false, and that flag stays visible in /r/index.json either way.

What you get

Source you can read. A health component's behaviour is a safety-relevant detail. When a RangeBar decides how to render a value with an unknown reference range, you should be able to read that decision rather than infer it from a prop table. Copied source makes that the default rather than an archaeology exercise through a bundle.

No version conflicts. Copied code has no peer dependency, cannot be duplicated in a lockfile at two versions, and cannot break because a transitive dependency of a dependency changed. In a long-lived clinical product this matters more than it does elsewhere; these codebases are maintained for years by people who did not write them.

Modification without forking. Changing a component is editing a file. There is no wrapper, no styled(...), no shadow DOM, no !important cascade fight.

A machine-readable inventory for free. Because the registry is a spec other tools already implement, the shadcn CLI and its MCP server work against opsinjs with no adapter. That is most of how Agents & automation gets to be short.

What it costs

Three costs, stated plainly.

You do not get updates automatically. Copied code is your code. An improvement here does not reach you until you deliberately take it. That is the entire point and it is also the largest ongoing cost, which is why Upgrades and diffs is not optional reading.

You inherit the maintenance. A security fix in a copied file is your job. There is no npm audit line that tells you a component you copied has a problem.

Drift is invisible without help. Two teams in one company copy the same component in different months and now have two different components with one name. Nothing in the file makes that visible, so what does is your own record of what you took and when: commit the files shadcn add writes, in their own commit, so a later --diff has something to compare against.

The shape of the registry

Three kinds of thing are distributed, addressable at stable paths:

PathWhat
/r/registry.jsonThe catalog, which lists every item and carries no file contents. What the CLI and the MCP server read first
/r/<name>.jsonOne item at the default base and style, with file contents inlined
/r/styles/<style>/<name>.jsonThe same item at an explicit style, for the base × style matrix
/r/themes/<preset>.jsonA theme-only item: cssVars and no files
/r/index.jsonThe opsinjs view of status, category and aliases, which the shadcn spec has no field for

The last row is worth noticing. registry.json is somebody else's schema and opsinjs does not extend it with private fields, because an item that fails validation elsewhere is worse than a second file. index.json carries what the spec does not.

Install from opsinjs

Two forms. The full URL needs no configuration, for an item that depends on nothing else here:

npx shadcn@latest add https://opsinjs.pensievelabs.org/r/status-pill.json

The namespaced form is shorter and requires configuration first:

npx shadcn@latest add @opsinjs/status-pill

Add the namespace to your components.json to use the second form, and to install any component that builds on another, whichever form you name it by:

components.json
{
  "registries": {
    "@opsinjs": "https://opsinjs.pensievelabs.org/r/{name}.json"
  }
}

Twenty-one items list their siblings in registryDependencies, and they list them namespaced as @opsinjs/status-pill rather than a URL, so the CLI needs the registries block above in order to follow them, even when you named the item by its full URL. Those twenty-one are alert-banner, card, care-card, consent-sheet, dialog, disclaimer-note, dose-tracker, empty-state, icon-button, link, log-sheet, metric-tile, range-bar, reading-input, result-card, score-dial, sheet, source-citation, tab-bar, timeline-entry and trend-sparkline. Configuring the namespace once is less work than remembering which list a component is on.

Namespaces covers composing several registries, private ones with authentication, and why @opsinjs is spelled the way it is.

Verify it worked

The catalog resolves

curl -s https://opsinjs.pensievelabs.org/r/registry.json | head -20

You should get a name, a homepage and an items array.

The CLI can see it

npx shadcn@latest search @opsinjs

Sixty items, every one of them implemented. implemented in /r/index.json records that for each. An error naming the namespace means components.json was not read.

add behaves honestly

Add any component and you get its source: the component file plus lib/opsinjs.ts and lib/status.ts, in the directories your components.json aliases name. Every id resolves to files today. Were a planned id added, the CLI should resolve the item and report that there is nothing to write, rather than creating an empty file.

Troubleshooting

add wrote nothing. Every id carries files today, so an empty write points at your components.json instead. Confirm its aliases, and check implemented for that name in /r/index.json.

@opsinjs is not found. Missing or malformed registries block. The {name} placeholder is required; a URL without it cannot be substituted into. The same error at the second step of an install has the same cause. At that step the item resolved and one of its registryDependencies did not, so add the block and retry.

A file was written to the wrong directory. The CLI uses the aliases in your components.json. See components.json.

I want a specific style. Use the /r/styles/<style>/<name>.json path. The docs URL for a component never carries a style segment. One canonical URL per component was a deliberate decision, and the matrix lives on the machine surfaces instead.

Next

  • Namespaces shows how to configure @opsinjs and compose registries.
  • registry.json is the catalog, annotated field by field.
  • Upgrades and diffs covers the fork problem, and the habit that makes it tractable.

On this page