Facetguides

Updating copied components

There is no facet upgrade. What diff and remove can tell you, what they deliberately cannot, and why there is no lock file.

The copy-in model means Facet can never push an update. A component in your project is a file in your repository; nothing reaches back into it. What the CLI offers instead is visibility — and it is careful about how much it claims.

facet diff

Terminal window
npx facet-rbxts diff # every installed component
npx facet-rbxts diff button # one

Compares each copied file against what the registry would write there today, replaying the ~/ rewrite first so your project’s alias and directory choices are not reported as changes.

The direction is registry → project: - is what the registry has, + is what your file says. That way applying the + side is what “keep mine” means, and a diff of your own edits reads the way git diff would have shown you making them.

A component in the registry that is not in your project is reported as such and skipped. A component you never added produces nothing — that is not news.

Taking an upstream change

There is no command for this, and that is the model working as intended:

  1. facet diff button — see what moved.
  2. Decide. Most upstream changes to a component you have edited are not worth taking.
  3. If you want it: facet add button --overwrite replaces the file wholesale, and your edits are gone. Commit first, then reapply what you want from the diff.

--overwrite is deliberately blunt. There is no three-way merge because there is no base text to merge against — see below.

facet remove

Terminal window
npx facet-rbxts remove badge
npx facet-rbxts remove badge --force

Two refusals, and they are not the same kind of refusal.

A file that differs from the registry is refused without --force. “Differs” is the closest available stand-in for “you changed this”, and it also fires when only upstream moved — so in the worst case you are asked about a file you never touched. That is the safe direction to be wrong in: asking too often is recoverable, deleting someone’s edited component is not.

A component another installed component still imports is refused, and --force does not cover it. Deleting utils out from under an installed button leaves a project that does not compile, and you asked to remove one component, not to break another.

That second check runs to a fixed point, which matters more than it sounds:

Terminal window
npx facet-rbxts remove button utils

If button turns out to be modified and so stays, utils is suddenly still imported — and a single pass would already have cleared it. The plan is recomputed until nothing changes.

Directories are left alone, and so is anything you added next to the removed files.

Why there is no lock file

A facet.lock of per-file content hashes is the obvious fix for what diff cannot say, and it was the plan. Three reasons it was dropped:

A hash answers whether, and a diff has to show how. Showing what a merge would involve needs the text you started from — three-way, base included. So the record that would actually satisfy facet diff is not a hash but a second copy of every component, committed to your repository and kept in step with the first. That is vendoring the registry into the project the CLI just copied out of, to serve one command.

Even the yes/no is weaker than it looks. The hash goes stale for reasons that are not edits: a project-wide formatter pass over src/shared/ui, a rename, a move to another directory. Facet would then report that you changed everything — true, useless, and indistinguishable from the report it would give if upstream had rewritten every component.

It is a file you commit, never edit, and cannot read. The promise of the model is that a copied file is yours. A lock file is Facet keeping a ledger about files it gave away, and the first time it disagrees with reality — and it will — it is a puzzle in your repository that Facet put there.

Pinning a registry revision

The default registry moves: https://facet.astra-void.xyz/r is republished on every push to Facet’s main, so an edit to a component reaches everyone immediately — including a project that ran facet add months ago and will next run facet diff.

Every push also writes an immutable copy under the commit that produced it, and a project can point at one instead:

facet.json
{ "registry": "https://facet.astra-void.xyz/r/a1b2c3d" }

revisions.json lists what exists, newest first. add, diff, list and doctor all read the field and all print which registry they used.

What pinning fixes: facet diff stops reporting upstream movement you did not ask for. Against a frozen revision, a difference is your edit or nothing — which is most of what the lock file was supposed to buy, without a lock file.

What it does not fix: it is per-project, not per-component. Pinning says “this project builds against that registry”, not “button came from that revision and card from this one”. And a pinned project stops receiving fixes until someone moves the pin, which is a decision somebody has to remember to make.

The thing revisions genuinely unlock is that the base text becomes something the CLI could one day fetch rather than something you store. That is not implemented, and it is the reason the versioning scheme was worth building the way it was.

What to actually do

  • Commit the copied files. They are source, not build output.
  • Do not expect updates. If a component is a starting point you rewrote, diff will be noisy forever and that is fine — stop running it on that component.
  • Run facet doctor after upgrading anything. It is the check that catches a project set up by an older CLI: the copied files are current and the versions under them are not.