Facetreference

CLI

Every command and flag of facet-rbxts, and what each one checks before it writes.

The package installs as facet-rbxts and the command is facet. Node 20+, ESM.

Terminal window
npx facet-rbxts <command> [options]
CommandWhat it does
initWrites facet.json, creates vela.config.ts if absent, installs build dependencies
addCopies components in, with their registry dependencies
listShows every component in the registry
removeDeletes copied components
diffShows how a copied component differs from the registry
doctorChecks the project matches what components assume

Global options

Option
--cwd <dir>Run against another directory. The project root is the nearest package.json at or above it.
--registry <url|path>Read from another registry — see Using another registry
--version, -v
--help, -h

Every command resolves the project root by walking up from --cwd (or the current directory) to the nearest package.json, and fails with No package.json found above … if there is none.

init

Terminal window
npx facet-rbxts init [-y] [--force] [--no-deps]
Option
--yes, -yAccept every default instead of prompting
--forceOverwrite an existing facet.json
--no-depsSkip every package install

Prompts for the theme base (zinc / slate / stone / neutral), the mode (dark / light), the three directories, and an optional import alias — leaving the alias blank means relative imports, which need no tsconfig paths.

Then, in order:

  1. Writes facet.json.
  2. Creates vela.config.ts pre-wired with facetTheme() only if it does not exist. An existing one is never rewritten — if it does not use facetTheme, the exact lines to add are printed.
  3. Installs @facet-ui/theme and vela-rbxts@^0.9.0 as dev dependencies.
  4. Runs add utils, because every component imports ~/lib/utils. This is also what installs @facet-ui/react-variants.
  5. Checks tsconfig.json for vela-rbxts/transformer and prints the snippet if it is absent.

Refuses to run if facet.json already exists, unless --force.

add

Terminal window
npx facet-rbxts add <names...> [--overwrite] [--dry-run] [--no-deps] [--yes]
Option
--overwriteReplace files that already exist instead of skipping them
--dry-runResolve and report, write nothing
--no-depsSkip the package install
--yesAnswer the provider-wiring prompt with yes instead of asking

Resolves the named items plus their registryDependencies, transitively and in dependency order. For each file: rewrites ~/ imports for where the file will land, then writes it. All writes go through one transaction that commits or rolls back as a unit.

Files that already exist are skipped, not overwritten, and reported as exists. --overwrite replaces them wholesale — there is no merge.

The union of the install set’s npm dependencies is then installed, through whichever package manager your lockfile implies (pnpm-lock.yaml → pnpm, yarn.lock → yarn, bun.lockb/bun.lock → bun, package-lock.json → npm; then the packageManager field; then npm).

Finally, if vela.config.ts does not use facetTheme and the added components name tokens, it says so with the token list — rather than letting the next build be the messenger with a Vela diagnostic on a file you never wrote.

Wiring a provider

If an added component declares a providers entry — today that is dialog and its PortalProvideradd offers to wrap your client entry in it. This is the one file the CLI edits that it did not write, and the bar for doing so is deliberately high.

PortalProvider has to wrap your app — Lattice reads the portal target from it,
and throws when a dialog opens without one
? Add it to src/client/main.client.tsx? (Y/n)

Why this and nothing else: every other thing the CLI reports is a build-time failure. A missing transformer means every class is inert on the next rbxtsc. A missing token is a Vela diagnostic. Both are loud, and both land in front of the person who just ran the command. A missing PortalProvider compiles, type-checks, passes CI, ships — and throws the first time a player opens the dialog, hundreds of lines of package-manager output after the snippet would have been printed.

The parser is used for positions, never for output. @babel/parser answers where the last import ends and where the render call’s argument starts; the edit itself is two string splices. Everything outside those two offsets comes out byte for byte identical — no re-quoted strings, no moved comments, no re-indented JSX on a file you arranged deliberately. A test asserts the exact bytes.

Anything ambiguous is reported, not guessed:

What the entry looks likeWhat happens
One .render( call, a PlayerGui expression somewhereWrapped, after the prompt
Two files under src/ mount a treeSnippet, naming both
Two .render( calls in the one entrySnippet — which tree to wrap is a design question
No PlayerGui named anywhereSnippet — synthesizing the lookup means editing imports too
The entry does not parseSnippet
Provider already importedNothing, and it says so

The PlayerGui case is the one worth explaining. Writing Players.LocalPlayer.WaitForChild("PlayerGui") into the file also means adding Players to an @rbxts/services import that may or may not exist — a second, riskier edit for a situation that barely occurs, since a client that mounts React always names its PlayerGui already. So the CLI reuses the expression that is there and stops when there is none.

facet doctor checks the same thing on every run, so a provider that gets refactored out later is reported rather than rediscovered at runtime.

Multiple providers are wired one at a time, and the entry is re-read between them — the second one wraps the file the first just rewrote.

list

Terminal window
npx facet-rbxts list [--registry <url|path>]

Every item in the registry with its description, alphabetically, plus which registry was read.

Inside a project it reads that project’s registry field, so a project pinned to a revision or pointed at a fork is shown what it can actually add. Outside one it falls back to the default — a missing or unreadable facet.json is an ordinary outcome here, not an error, because listing what exists is what you run before init.

remove

Terminal window
npx facet-rbxts remove <names...> [--force]
Option
--forceDelete a file that differs from the registry

Removes exactly what you name. Dependencies are not pulled in — that would delete utils because you removed button.

Two refusals:

  • A file that differs from the registry, compared after replaying the same ~/ rewrite add applied on the way in, so a file that was only ever copied reads as untouched. --force overrides this.
  • A component another installed component still imports. --force does not override this one. The check runs to a fixed point: if one item stays behind because it was modified, anything it imports stops being free to delete.

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

diff

Terminal window
npx facet-rbxts diff [name]

With no argument, walks everything the registry knows about that also exists in the project.

Compares each file against what the registry would write there today, replaying the ~/ rewrite first so your alias and directory choices are not reported as changes. Direction is registry → project: - is the registry, + is your file.

Facet records nothing at copy time, so this cannot tell your edit from an upstream change — and it says so in its own output rather than implying otherwise. See Updating copied components.

doctor

Terminal window
npx facet-rbxts doctor

Eight checks, in order. Each is ok, warn, or fail; any fail exits non-zero.

CheckFails when
facet.jsonnever — reports style, theme, and whether each alias directory exists
import aliasfacet.json sets an import and tsconfig declares no paths entry that could resolve it
transformertsconfig.json does not register vela-rbxts/transformer
themevela.config.ts does not exist (a config that exists but does not use facetTheme is a warning)
componentsnever — lists what is installed; warns when an item is missing some of its files
tokensan installed component names a token the theme does not define
packagesa package is missing, or older than the floor the copied files need
providersan installed component declares a provider and the client entry is not wrapped in it

The providers check is the only one whose failure is not a build failure. A dialog with no PortalProvider above it compiles, ships, and throws the first time a player opens it — and nothing in the copied file can prevent that, because the wiring lives in a file Facet does not own. When it fails it prints the snippet; when it cannot find a single client entry to look at, it warns rather than failing, and says to check by hand.

The packages check is the one that catches a project set up by an older CLI: the copied files are current and the versions under them are not. It reads the version actually sitting in node_modules — walking up, because a project inside a workspace has its dependencies hoisted — rather than the declared range, since the two differ exactly when someone installed once and never again.

A registry it cannot reach is a gap in the report, not the end of it: the first four checks stand on their own, and the last three are skipped with a warning saying so.

Version comparison is deliberately not semver

The comparison answers one question — is what is installed at least the version this needs? — for the shapes Facet actually writes (^0.9.0, >=0.8.0, 0.1.1). Anything it cannot read is reported as unverifiable rather than guessed at, because a doctor that invents a failure is worse than one that admits a gap.

Environment variables

Variable
FACET_REGISTRY_DIRA local directory. Wins over everything, including --registry.
FACET_REGISTRY_URLA URL. Loses to --registry and to facet.json.

Exit codes

0 on success. 1 on a user-facing error — an unknown command, an unreachable registry, a failed doctor. Anything else is a bug and throws with a stack.