Skip to main content

Usage

Checks every registry-backed facet in facets.json for a newer release and installs the ones that move. facet upgrade is an alias of the same command — same flags, same output, same exit codes. This updates the facets your project declares. To update the facet binary itself, use facet self-update. facet update does not accept positional arguments. To update some facets but not others, use --interactive.

Examples

Flags

boolean
Update to each facet’s latest release, ignoring the range in facets.json. Short form: -L.Because the selected version may fall outside what the project declares, this is the only mode that rewrites the specifier — see Crossing the declared range.It has no effect alongside --interactive, which already opens every row on its latest release. The combination is accepted, not rejected.
boolean
Choose which facets to update, and which version each one takes. Short form: -i.Every row opens on its latest release with nothing selected, so Space takes the newest version of that facet — see Choosing interactively.Requires a terminal that can prompt. Without one, the command fails before contacting the registry rather than after.
boolean
Print the plan; do not modify any files. Exits 0 whether or not updates are available.
boolean
Show detailed step output on stderr. Verbose output never reproduces an MCP server declaration — see Where declarations appear.
boolean
Approve the MCP server configuration this update would write, without prompting.An update is an install, so it collects the same approval on the same terms as facet install. It does not authorize an asset takeover or resolve an asset name collision.
There is no --frozen-lockfile. Frozen mode reproduces what the lockfile already records, which is the opposite of what this command does. It belongs to facet install.

Exit codes

Current, Target, and Latest

Every checkable facet has three versions, and the plan shows all three:
installed
The exact version facets.lock records as installed.
range-respecting
The newest release the specifier in facets.json already permits. Selecting it never changes what the project declares.
newest published
The registry’s newest release, regardless of the declared specifier.
All three are shown even for a facet that is not moving, because that is what answers “why is this one staying put?”:
Here viper-plans moves to 1.4.0 under a plain facet update. cowsay does not move at all: its range allows nothing newer than what is installed, and reaching 3.0.0 requires --latest. Plain facet update takes every Target that advances. --latest takes every Latest that advances.

Crossing the declared range

--latest may select a version the declared specifier forbids, so it rewrites that specifier — by the smallest edit that admits the new version while preserving how you chose to express intent:
A pin stays a pin, a major wildcard stays a major wildcard, and a specifier that already floats is left exactly as written. The complete table lives in the manifest-write policy. Selecting a Target never rewrites anything: the specifier already permits that version, so the declared intent has not changed.

Choosing interactively

--interactive lists every facet with an advancing version and lets you pick both which facets move and which version each one takes:
Every row shows all three versions at once, because the choice this screen exists for is a comparison. Only the digit that actually moves is coloured — green for a patch, amber for a minor, coral for a major — so the size of each jump is visible without reading the numbers twice. The column a row would install is named in words, so the screen still works with colour disabled. Every row starts on its Latest, with nothing selected. Move down the list and press Space on the facets you want; each one takes its newest release. Nothing is chosen for you, so Space always means “yes, this one” — and a facet you never touch is simply left alone. --latest changes none of this; it is how the non-interactive run says what this screen already offers. ↑↓ moves between facets, wrapping at the ends. ◀ ▶ moves between the two columns and clamps instead, so holding one down settles on a column rather than oscillating. l also flips the focused row. Move a row to target before selecting it to take the version your declared range already permits instead. A row showing a version that is already installed cannot be selected — pressing Space there says so rather than silently doing nothing. That is what you see on a pinned facet after moving it to target. Confirming requires at least one selected facet. Selection happens before adapter selection and before anything is written, so Esc or Ctrl-C costs nothing: the command reports that nothing was applied and exits 1. Without a terminal that can prompt — CI, piped output — --interactive fails immediately, before any registry lookup, and points at plain facet update or facet update --latest.

Previewing

--dry-run prints the plan and modifies nothing: no manifest, no lockfile, no receipt, no assets, no cache, and no adapter installation. Combined with --interactive, it previews the selection you confirmed and stops there. A preview always prints the plan, including when nothing would move — with no row marked and no facets.json edit shown, followed by the reason. The plan is what the reason was read off: which facet is pinned, which is already current, which range is holding one back. A run without --dry-run prints the reason alone.
A preview shows versions, not their downstream effects. Which assets a new release materializes, whether it collides with another facet, and what MCP configuration it declares are all determined during the install that a real run performs.

What it does

1

Discover, without taking the project lock

Reads facets.json and facets.lock, then asks the registry for each facet’s range Target and its Latest. This phase writes nothing and takes no lock, so reading a plan never blocks another facet operation on the machine. A single failed lookup rejects the whole discovery rather than presenting a partial answer.
2

Select

Applies the mode’s default choices, or the ones you confirmed in the picker. A --dry-run stops here.
3

Apply through the install pipeline

Confirms the project has not changed since the plan was reviewed, then runs the ordinary install pipeline for the selected facets — the same verification, collision detection, MCP approval, materialization, rollback, and atomic tri-write as every other install. The reviewed version is installed as chosen; a release published between review and application does not change it.
The summary reports each facet’s move:
Recorded materialization choices — aliases and omissions — survive a version change. Outcomes, counts, and collision behavior are identical to facet install.

When nothing moves

A run that applies nothing still succeeded, and exits 0. Which kind of nothing it is decides whether you have anything to do about it. Under --dry-run the plan is printed first, so you can check the reason against the rows it came from:

Sources that cannot be checked

Only registry facets have releases to compare. Git and local facets are named in the plan rather than counted as current:
Reporting them as up to date would claim something nothing verified. Move one by editing its source in facets.json and running facet install.

When the project cannot be checked

If a declared registry facet is missing from facets.lock, locked to a different kind of source, locked at something that is not an exact release, or locked at a version its specifier no longer permits, update refuses to guess and exits 1. It names every affected facet, because one facet install covers all of them:
Repairing lockfile drift is facet install’s job, not this command’s.

When the project moves mid-run

If facets.json or facets.lock changes between reading the plan and applying it, the update is withdrawn before anything is written and exits 1. Nothing is broken and nothing needs repairing — the plan simply described a project that no longer exists. Re-run facet update to plan against the current state.

Project facets vs. the CLI binary

Four command names sit one keystroke apart. The self- prefix is what separates them: Neither touches the other.

See also