@formeui/cli
v0.1.2
Published
The forme CLI. Installs Free and Pro Forme source into a project.
Maintainers
Readme
@formeui/cli
The forme CLI, published to npm as @formeui/cli. It installs Forme source into a project and then gets out of the
way: after an install nothing from @forme/* is needed at runtime. The CLI is
a delivery mechanism, not a dependency.
npx @formeui/cli add button # Free, public, MIT
npx @formeui/cli login # sign in, once
npx @formeui/cli add analytics-dashboard # Pro, needs a licenceInstalled globally (npm i -g @formeui/cli) the command is plain forme, which
is what the rest of this file writes. The npm name forme belongs to an
unrelated package, so never document npx forme.
Why the CLI splits in two
Forme has two tiers and they are delivered differently, on purpose.
Free components are public and already install with the shadcn CLI. So the
CLI hands them over rather than reimplementing the installer: it writes a
temporary components.json with a registries entry and runs
npx shadcn@latest add @forme/<name>, then puts your components.json back
byte for byte. Owning prompts, diffs, --overwrite and the Tailwind check is
shadcn's job, and delegating means Forme is not on the hook when shadcn changes.
Pro components are the product, so they come from the authenticated API and are written by this CLI. Nothing is public, and there is no npm package to pull into your app.
forme add button
-> writes a temporary components.json with "@forme"
-> npx shadcn@latest add @forme/button --yes
-> restores components.json exactly as it was
forme add analytics-dashboard
-> POSTs your bearer token for the Pro item
-> installs forme-foundation first, then the item
-> writes each file to its @components/ target
-> prints the npm packages to installCommands
forme add <name...>
| Option | Meaning |
| ---------------------- | ----------------------------------------------------------------------------------------------- |
| --dry-run | print the plan and write nothing |
| --registry-url <url> | where Free components come from. Default https://formeui.com, or FORME_REGISTRY_URL |
| --api-url <url> | where the Pro registry and CLI auth live. Default https://api.formeui.com, or FORME_API_URL |
Names may be given as button or @forme/button. An unknown name is reported
with the closest names in the index, and the names that were understood still
install; the exit code is 1 when anything was missing.
Pro installs write to the @components/ alias from your components.json, with
src/ layouts, custom aliases and tsconfig paths all resolved. Dependencies
declared in a registry item are installed first, so a file is never written before
the file it imports. npm packages are printed for you to install rather than added
to package.json: Forme never adds a runtime dependency.
forme login
The browser hand-off, end to end:
- The CLI listens on
127.0.0.1on an ephemeral port. - It generates a random
stateand opens<siteUrl>/cli/auth?port=<port>&state=<state>. The URL is printed either way, so a machine with no browser to open still works. - You sign in on the site and approve the CLI. The site writes a single-use code that expires in five minutes.
- The site sends the browser to
http://127.0.0.1:<port>/callback?code=<code>&state=<state>. - The CLI checks
state,POSTs the code to/api/cli/authorize, saves the token, prints a small page and exits.
Checking state is what stops another website from handing your browser a code it
did not ask for. The local server binds to loopback only and is closed on every
exit path, including Ctrl-C. Five minutes is the timeout.
| Option | Meaning |
| ------------------ | -------------------------------------------------------------------------------- |
| --site-url <url> | where the browser goes to sign in. Default the registry URL, or FORME_SITE_URL |
| --api-url <url> | where the token exchange happens |
forme logout
Deletes the local token, and revokes it server-side when it can reach the API. The local file goes first, so a network failure cannot strand a credential on disk; if the revoke did not happen, the CLI says so and tells you to run it again when you are online.
forme whoami
The signed-in email and entitlements. --json prints them raw.
forme list
Every component, marked free or pro. --tier free|pro filters, --json
prints raw. Pro names only appear when you are signed in, because a Pro item must
not be discoverable from the public registry, not even by name.
forme search <query...>
Matches name, title, description and category. All words must match.
forme init
Adds the @forme registry to an existing components.json, permanently, and
prints the two Tailwind v4 steps:
@import "tailwindcss";
@import "../components/forme/foundation.css"; /* adjust to your globals.css */forme add forme-foundation writes that CSS file for you.
Config
~/.config/forme/config.json, mode 0600 on POSIX because it holds a
credential.
{ "token": "...", "expiresAt": "2026-12-01T00:00:00.000Z", "email": "[email protected]" }FORME_CONFIG_DIR points it somewhere else, which is how you keep two accounts or
run the CLI in a test. XDG_CONFIG_HOME is honoured when it is set.
Environment
| Variable | Default | What it does |
| -------------------- | ------------------------- | ------------------------------------------------------- |
| FORME_CONFIG_DIR | ~/.config/forme | Where the token is stored |
| XDG_CONFIG_HOME | unset | Used to find the config directory |
| FORME_REGISTRY_URL | https://formeui.com | Where Free components come from |
| FORME_API_URL | https://api.formeui.com | Where the Pro registry and CLI auth live |
| FORME_SITE_URL | the registry URL | Where the browser goes to sign in |
| FORME_INDEX_PATH | unset | Read the index from a local file instead of the network |
FORME_INDEX_PATH
Points forme list, forme search and the tier lookup in forme add at a local
index file instead of fetching one. The file is the shadcn registry index shape
that packages/registry/dist/registry.json is written in:
{
"name": "forme",
"homepage": "https://formeui.com",
"items": [
{
"name": "button",
"title": "Button",
"description": "The primary action element.",
"type": "registry:ui",
"tier": "free",
"category": "buttons",
"dependencies": ["clsx"],
"registryDependencies": ["forme-foundation"]
}
]
}So forme list works with no network at all:
FORME_INDEX_PATH=packages/registry/dist/registry.json forme listOnly the index is read from that file. forme add <pro-name> still fetches the
item itself from the API, because source must never sit in a file you can commit.
How it runs
In this repo, bin/forme.mjs imports ../src/cli.ts and lets Node strip the
types, so working on the CLI needs no build step. On a Node too old to strip
types it re-executes itself with --experimental-strip-types, passing arguments
and the exit code through.
Published, it cannot do that: Node refuses to strip types from files under
node_modules. So prepack compiles src/ to dist/ (tsconfig.build.json)
and the package ships bin/ and dist/ only. The shim runs the source when it
exists and the build otherwise, so a stale local dist/ never shadows src/.
Everything the CLI receives from outside is validated with zod before it is
used: the registry index, the registry item, the authorize and whoami replies,
and your components.json. A malformed registry item is a bad gateway, not a file
to install.
Layout
bin/forme.mjs the shim: the TS source here, the compiled dist/ when published
src/cli.ts commander wiring, one function per command
src/catalog.ts the index: Free from the site, Pro from the API, or FORME_INDEX_PATH
src/api.ts fetch + zod schemas for every Forme response
src/install.ts resolve the item graph, write files, print npm packages
src/free.ts the shadcn CLI hand-off, and `init`
src/login.ts the local server, the state check and the token exchange
src/project.ts components.json and tsconfig alias resolution
src/config.ts ~/.config/forme/config.json and the URL defaults
src/output.ts plain text output and FormeErrorReleasing
Bumping the version is the release. Nothing else publishes.
pnpm cli:version patch # or minor, major, or prerelease --preid=beta
git commit -am "CLI: 0.1.1" && git pushOn main, .github/workflows/publish-cli.yml runs whenever this
package.json changes. It publishes only if npm does not have that version yet:
it type-checks, tests, packs (which builds dist/), installs the tarball in an
empty project and runs it, then publishes and tags cli-v<version>. A
prerelease version goes out under the next dist-tag, never latest. To retry
a failed publish, run the workflow by hand.
Check a package before you release it:
cd packages/cli && pnpm pack # then npm i ./formeui-cli-<version>.tgz somewherePublishing is token-free. The workflow authenticates with npm trusted
publishing (OIDC): @formeui/cli trusts GitHub Actions in dvaJi/forme
running publish-cli.yml, and nothing else. No npm token is stored anywhere.
It is set up on npmjs.com under the package's Settings → Trusted publishing,
and npm accepts it before a package's first publish, so 0.1.0 went out through
the workflow too. If the workflow is renamed or moved, update that setting or
the next release fails with an auth error.
