---
title: "Upgrades and diffs"
description: "How to take an update to code you own and have already edited, without either losing your changes or silently keeping a bug."
url: "https://opsinjs.pensievelabs.org/registry/upgrades-and-diffs"
source: "https://opsinjs.pensievelabs.org/registry/upgrades-and-diffs.md"
section: "Registry & distribution"
kind: "guide"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["upgrade", "update components", "three-way merge", "--diff", "fork problem", "file provenance"]
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> Nothing is missing from this page. The data simply does not live in
> the prose.

<PageTemplate kind="guide" />

## Overview [#overview]

In a copy-not-dependency model there is no `npm update`. A fix here does not
reach you; you go and get it, into a file you may have edited, in a codebase
somebody else now maintains.

This is the central cost of the distribution model and it is worth taking
seriously rather than discovering during an incident. The good news is that it
is a merge problem, and merge problems have known tools. The bad news is that
nothing will tell you an upgrade exists unless you build the habit of asking.

### Which version do I have? [#which-version-do-i-have]

There is no lockfile here, because there is no dependency to lock. Nothing in an
installed file records where it came from, and no tool can reconstruct it after
the fact. The record that exists is your own commit history, so make it carry
the answer: &#x2A;*run `add` in its own commit, with nothing else in the diff, and put
the item name and the date in the message. Your own changes go in the next
commit.** That one habit is the provenance of every copied file you own. It
gives a later three-way merge a base to work from, it tells a reviewer which
hunks arrived from upstream and which are yours, and it is the only thing a
colleague can read a year from now to find out what you took.

`npx shadcn@latest add @opsinjs/<item> --diff` is a different question and not a
substitute. It tells you how your file differs from upstream *today*. It cannot
tell you which upstream you started from, so it cannot separate a local edit
from an upstream change you have not taken yet. The commit answers where you
started; the diff answers where you have drifted to.

## The upgrade loop [#the-upgrade-loop]

<Steps>
  ### Find out what changed [#find-out-what-changed]

  Read the [changelog](../project/changelog/index.mdx), then read the commit that
  installed the file. Those two are the whole answer. Do this before touching
  anything; an upgrade you cannot describe is one you cannot review.

  ### Diff before you fetch [#diff-before-you-fetch]

  <CodeBlockTabs defaultValue="npm" groupId="package-manager">
    <CodeBlockTabsList>
      <CodeBlockTabsTrigger value="npm">
        npm
      </CodeBlockTabsTrigger>

      <CodeBlockTabsTrigger value="pnpm">
        pnpm
      </CodeBlockTabsTrigger>

      <CodeBlockTabsTrigger value="yarn">
        yarn
      </CodeBlockTabsTrigger>

      <CodeBlockTabsTrigger value="bun">
        bun
      </CodeBlockTabsTrigger>
    </CodeBlockTabsList>

    <CodeBlockTab value="npm">
      ```bash
      npx shadcn@latest add @opsinjs/result-card --diff
      ```
    </CodeBlockTab>

    <CodeBlockTab value="pnpm">
      ```bash
      pnpm dlx shadcn@latest add @opsinjs/result-card --diff
      ```
    </CodeBlockTab>

    <CodeBlockTab value="yarn">
      ```bash
      yarn dlx shadcn@latest add @opsinjs/result-card --diff
      ```
    </CodeBlockTab>

    <CodeBlockTab value="bun">
      ```bash
      bun x shadcn@latest add @opsinjs/result-card --diff
      ```
    </CodeBlockTab>
  </CodeBlockTabs>

  This shows what would change in your file without writing anything. It is the
  single most useful flag in the CLI for this model, and the one people discover
  last.

  ### Decide per hunk, not per file [#decide-per-hunk-not-per-file]

  Some hunks are upstream fixes you want. Some are upstream style changes you do
  not care about. Some collide with a deliberate local change. Treat them
  separately. The temptation to accept or reject the whole file is what produces
  both lost fixes and lost customisations.

  ### Take it [#take-it]

  <CodeBlockTabs defaultValue="npm" groupId="package-manager">
    <CodeBlockTabsList>
      <CodeBlockTabsTrigger value="npm">
        npm
      </CodeBlockTabsTrigger>

      <CodeBlockTabsTrigger value="pnpm">
        pnpm
      </CodeBlockTabsTrigger>

      <CodeBlockTabsTrigger value="yarn">
        yarn
      </CodeBlockTabsTrigger>

      <CodeBlockTabsTrigger value="bun">
        bun
      </CodeBlockTabsTrigger>
    </CodeBlockTabsList>

    <CodeBlockTab value="npm">
      ```bash
      npx shadcn@latest add @opsinjs/result-card --overwrite
      ```
    </CodeBlockTab>

    <CodeBlockTab value="pnpm">
      ```bash
      pnpm dlx shadcn@latest add @opsinjs/result-card --overwrite
      ```
    </CodeBlockTab>

    <CodeBlockTab value="yarn">
      ```bash
      yarn dlx shadcn@latest add @opsinjs/result-card --overwrite
      ```
    </CodeBlockTab>

    <CodeBlockTab value="bun">
      ```bash
      bun x shadcn@latest add @opsinjs/result-card --overwrite
      ```
    </CodeBlockTab>
  </CodeBlockTabs>

  Then re-apply your local changes from the diff you just read. If your local
  changes are extensive, a three-way merge is better. See below.

  ### Re-check [#re-check]

  Type-check, run your tests, and re-run the accessibility and contrast checks
  that apply to your project. An upgrade that changes a token pair changes a
  measurement.
</Steps>

## Three-way merge, when you have edited a lot [#three-way-merge-when-you-have-edited-a-lot]

`--overwrite` is a two-way operation: upstream replaces yours. When you have
made substantial deliberate changes, do it as a proper three-way merge instead.

The three sides are: the upstream version you originally copied, the upstream
version you want, and your current file. Any merge tool can take it from there,
and the result is a merge with conflicts you resolve rather than a choice between
two whole files.

This only works if you can identify and obtain the version you originally
copied, and nothing in the file tells you which that was. The commit that ran
`add` is the record, which is why it earns a commit of its own. It is also the
argument for opsinjs serving historical items rather than only the latest.

```bash
git merge-file --diff3 mine.tsx base.tsx theirs.tsx
```

## Make upgrades reviewable [#make-upgrades-reviewable]

Four habits that make the difference between a system that can be updated and
one that quietly ossifies.

**Commit the install.** In its own commit, as set out under
[Which version do I have?](#which-version-do-i-have) above. It is first here
because the other three are recoverable without it and nothing recovers it.

**Keep local changes small and marked.** A comment saying *why* the local change
exists survives a merge; a silent divergence does not, and the next person will
"fix" it back.

**Prefer composition over editing.** Wrapping a component, or passing different
content, produces no merge conflict at all. Editing its internals produces one
every time. The system is designed to be edited, but not editing it is still
cheaper.

**Schedule the ask.** Nothing pushes an update to you. A quarterly check of
what has changed upstream is a small habit that prevents a large migration.

## What an upgrade can break [#what-an-upgrade-can-break]

<Callout>
  No upgrade path has been exercised in anger: every catalogue row is still
  `since: unreleased`, so there is not yet a second version of anything to upgrade
  to. The list below is the set of hazards the design anticipates, and it is a
  specification of what upgrade notes will have to cover rather than a report of
  observed breakage.
</Callout>

* **A token pair moved.** Your contrast measurements are stale. Re-run them.
* **A data attribute changed.** If you styled against `data-status`, an attribute
  rename is a silent visual regression, not a type error.
* **A part was renamed in a compound component.** Wrong nesting produces a
  component that renders and does nothing, which is worse than one that throws.
* **A dependency was added.** `registryDependencies` grew, and `add` installed
  something else into your tree. Read the install output.
* **A clinical default changed.** The rarest and the most serious. Any such
  change carries a changelog entry with a safety note; it is worth reading the
  changelog for these alone.

## Verify it worked [#verify-it-worked]

<Steps>
  ### The diff is empty [#the-diff-is-empty]

  Re-run `add --diff` after upgrading. It should report no differences except your
  own deliberate local changes.

  ### The install is recorded [#the-install-is-recorded]

  Your commit should name the item and the date you took it, because the file will
  not. That record is what the next upgrade's three-way merge needs, and
  reconstructing it afterwards is guesswork.

  ### Your local change is still there [#your-local-change-is-still-there]

  Search for it. This is the check people skip and the one that catches a lost
  customisation.
</Steps>

## Troubleshooting [#troubleshooting]

**`--diff` reports the whole file as changed.** Line endings or formatting.
Normalise both sides before comparing, and consider whether your formatter runs
over installed files.

**`--overwrite` destroyed a change I needed.** Recover from git. Then adopt the
commit-the-install-separately habit, which makes this recoverable by
construction.

**I cannot tell which version I originally copied.** Nothing in the file says,
so unless your commit history does, there is nothing to read. Diff against the
current upstream and accept that this one is archaeology.

**Upgrading one component pulled in three others.** `registryDependencies`.
Composition is explicit, and this is what explicit looks like.

## Next [#next]

* [Changelog](../project/changelog/index.mdx) says what actually changed.
* [Versioning policy](../project/versioning-policy.mdx) says what a version
  number is allowed to imply.
