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:
| Path | What |
|---|---|
/r/registry.json | The catalog, which lists every item and carries no file contents. What the CLI and the MCP server read first |
/r/<name>.json | One item at the default base and style, with file contents inlined |
/r/styles/<style>/<name>.json | The same item at an explicit style, for the base × style matrix |
/r/themes/<preset>.json | A theme-only item: cssVars and no files |
/r/index.json | The 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.jsonThe namespaced form is shorter and requires configuration first:
npx shadcn@latest add @opsinjs/status-pillAdd 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:
{
"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 -20You should get a name, a homepage and an items array.
The CLI can see it
npx shadcn@latest search @opsinjsSixty 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
@opsinjsand 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.