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. It 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, the mode, 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, and the exact lines to add are printed instead.
  3. Installs @facet-ui/theme and vela-rbxts@^0.9.0 as dev dependencies.
  4. Runs add utils, since every component imports ~/lib/utils. This also installs @facet-ui/react-variants.
  5. Checks tsconfig.json for vela-rbxts/transformer and prints the snippet if 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, rewriting each file’s ~/ imports for where it will land. 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 selects pnpm, yarn.lock yarn, bun.lockb or bun.lock bun, and package-lock.json npm. Failing that it reads the packageManager field, then falls back to npm.

Finally, if vela.config.ts does not use facetTheme and the added components name tokens, it says so with the token list. Otherwise the next build reports it as a Vela diagnostic on a file you never wrote.

Wiring a provider

If an added component declares a providers entry, add offers to wrap your client entry in it. Today that is only dialog. This is the one file the CLI edits that it did not write, and the bar 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: everything else the CLI reports fails at build time. A missing PortalProvider compiles, type-checks, passes CI, ships — and throws the first time a player opens the dialog.

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 is two string splices, so everything outside those offsets comes out byte for byte identical.

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

Writing Players.LocalPlayer.WaitForChild("PlayerGui") would mean adding Players to an @rbxts/services import that may not exist. 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 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 pinned project is shown what it can actually add. Outside one it falls back to the default — a missing facet.json is an ordinary outcome here, since 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, 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. 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 today, replaying the ~/ rewrite first so your alias and directory choices are not reported as changes. Direction is registry → project.

Facet records nothing at copy time, so this cannot tell your edit from an upstream change, and says so in its own output. 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 compiles, ships, and throws the first time a player opens it. Nothing in the copied file can prevent that. When it fails it prints the snippet. When it cannot find a single client entry it warns rather than failing.

The packages check catches a project set up by an older CLI, where the copied files are current and the versions under them are not. It reads the version actually in node_modules, walking up for a workspace, rather than the declared range. 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 alone, and the last three are skipped with a warning.

Version comparison is deliberately not semver

The comparison answers one question for the shapes Facet writes. Is what is installed at least the version this needs? Anything it cannot read is reported as unverifiable rather than guessed at.

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.