Skip to main content

Better CLI

The better CLI puts a design system in your repository as files. You edit Liquid and JSON in your own editor, review changes in pull requests, and push a named version to Better Email when it is ready.

Nothing about the platform changes underneath. The files are the same design system the Design System Editor shows, versions still land in the same timeline, and publishing to Live is still something a person does in the app. What moves is the authoring surface.

Install

npm install -g @better-email/cli

It needs Node 20.16 or newer on the 20.x line, or Node 22.3 or newer. The package bundles everything it uses and has no runtime dependencies.

better --help

Design system commands live under the ds noun — better ds pull, better ds push. Inside a design system directory the bare verbs work as shortcuts, the way git push needs no noun, so better check and better ds check are the same command. Only better login sits outside the noun.

Sign in

better login

This opens your browser, asks you to confirm a code, and stores the result in ~/.config/better-email/credentials.json with mode 600. The session is yours: every version you push is attributed to you in the design system's history, and your role in the workspace decides what you can do.

Signing in is not needed for better check or better ds dev — both work offline, on any directory, with no credentials at all.

For automation, use an organization API key instead of a browser session. See CI recipes.

Start a checkout

The fastest path scaffolds a repository around an existing design system:

npm create better-email acme-design-system

It signs you in if you are not already, lists the design systems in your organization, pulls the one you choose, and adds an AGENTS.md describing the layout and the loop, a .gitignore, and a GitHub Actions workflow that runs better check on every pull request.

If you already have a repository, pull into it directly:

better ds list
better ds pull k97a1c8y3fq2m5r0n6w4hz7b

pull writes the managed files and records the binding in .better/config.json. Managed files that no longer exist on the platform are removed, so a module deleted in the app disappears from your tree. Everything outside the managed layout is preserved, and pull lists what it left alone.

By default pull takes the Live version. --channel candidate takes what is currently staged, and --channel latest takes the newest version whether or not it has been published. Pull latest when you are continuing work someone saved in the app; pull live when you want the tree campaigns are rendering right now.

Once a directory is bound, later pulls need no id — better ds pull refreshes the same design system. The channel does not carry over, though: a bare pull always takes Live, so repeat --channel when you want a different one.

Starting from Meridian

betteremail/meridian is a complete, MIT-licensed starter design system kept in this format: twelve modules, one template base, one set of brand tokens, built to be recoloured in an afternoon. It is the fastest way to a working tree, and the reference for how the files fit together.

Meridian is a set of files rather than a hosted template, so you adopt it by replacing the managed files of a design system you own:

git clone https://github.com/betteremail/meridian.git
better ds pull <id> --dir my-design-system
cp -R meridian/. my-design-system/
cd my-design-system && better check

Copying over a pulled tree keeps .better/ and anything unmanaged, so the checkout stays bound and better ds diff shows exactly what adopting Meridian would change before you push it.

Already have a build?

If your emails are built with Maizzle or React Email, do not start from a checkout at all: keep the project you have and generate the design system tree out of it, so your components and git history come with you. Each adapter has its own worked example repository.

Starting pointRepositoryFor
Hand-authored filesbetteremail/meridianA complete design system to recolour and ship, and the reference for the format.
Maizzle projectbetteremail/maizzle-starterTailwind fragments with a better: front-matter block. See migrating from Maizzle.
React Email projectbetteremail/react-email-starterReact components with a typed better export. See migrating from React Email.

All three are MIT licensed.

The loop

pull → edit → check → dev → diff → push

check

better check

Validates the local tree: file integrity, the JSON schemas, setting-key uniqueness, and module order. It needs no credentials and no binding, so it runs anywhere — a pre-commit hook, a pull request, a bare clone.

Errors name the file and say what to do:

Local checks only; the server validates the Design System again on push.
Duplicate setting key "hero":
modules/hero/settings.json
modules/feature_row/settings.json

Exit code 0 means the tree is valid, 1 means it is not.

--json prints one result object instead, for tools that need to read the findings rather than show them:

{"ok":false,"findings":[{"path":"modules/hero/settings.json","message":"Duplicate setting key \"hero\".","severity":"error"}]}

Each finding carries path, message, severity, and a line when the checker knows one. This is what the VS Code extension turns into inline diagnostics.

dev

better ds dev

Watches the managed files and serves a live preview with editable inputs. Rendering happens entirely on your machine — no credentials, no network. Save a file and the preview updates.

Local Design System preview: http://localhost:4100
Open in the app: https://app.better.email/design-systems/local-dev?server=4100
Watching managed files. Press Ctrl+C to stop.

The first URL is a standalone preview shell. The second opens the same local server inside Better Email, so you get the real module switcher and settings sidebar around your local files. It appears once you have run better login, and opening it needs an Admin or Designer role in the app. Your files are not uploaded to do this: the app page reads them from localhost in your own browser.

The default port is 4100. If it is busy the CLI slides to the next free port, so two previews can run side by side. An explicit --port fails instead of sliding. --open launches the browser for you.

If a module renders as nothing here and better check is happy, look at its first and last Liquid tags: whitespace control at either boundary makes a module vanish silently. See whitespace control at the module boundary.

diff

better ds diff

Compares the local tree with the channel you pulled, module by module:

+ added Seasonal Offer (cmp_seasonal_offer)
~ edited Hero (cmp_hero) [content, settings]
- removed Legacy Banner (cmp_legacy_banner)
~ Template Base: Base HTML, Content zones
~ Module order changed

Module rows are flagged with what changed — content, settings, behavior. The template base row names the parts that differ: Base HTML, Global settings, Content zones, Assets, Global values, or Metadata.

--against live|candidate|latest compares with a different channel. Exit code 1 means there are changes, 0 means there are none, which makes diff usable as a drift check in CI.

push

better ds push --name "Summer refresh"

Push prints the same diff, asks for confirmation in a terminal, and creates a version in the design system's history — a numbered, named entry, exactly like saving a version in the editor. Without --name, the CLI names it after the timestamp and the current git short SHA. --description adds a longer note.

If the organization has candidate testing enabled and the design system has a Live version to test against, push also stages the new version as the Candidate:

Created Version 12: Summer refresh.
Staged as Candidate.

Otherwise it says why it did not:

Created Version 12: Summer refresh.
Not staged as Candidate: Candidate testing is off for this Organization.

--no-stage creates the version without staging it. --yes skips the confirmation prompt.

Push never publishes to Live

The furthest a push can go is the Candidate slot. Making a version Live is a deliberate action taken by an Admin or Designer in the app, from the design system's version timeline. A merged pull request therefore cannot change what campaigns render.

Push is guarded against overwriting work you have not seen. It records which version was newest when you pulled, and refuses if the design system has moved on since:

This Design System changed since your last pull. Run `better ds pull` (or push --force).

It also refuses while someone holds the editor lock in the app, and reports that person by name. An unchanged tree pushes nothing at all.

status

better ds status
Design System: Acme Newsletter (k97a1c8y3fq2m5r0n6w4hz7b)
Channel: live
Pulled Revision: k1739fdz2xy8p4c0v6s3jr5q
Live: Version 11 — Spring layout
Candidate: none
Latest: Version 11 — Spring layout
Local tree: clean

Local tree: modified means your files differ from what you pulled. The three pointers are the platform's current view, so status is how you notice that someone published or staged something while you were working.

A full pass

Adding a testimonial module to a design system that has candidate testing on.

better ds pull k97a1c8y3fq2m5r0n6w4hz7b
git switch -c testimonial-module

Create modules/testimonial/ with its three files, then add testimonial to modules.order. Validate before rendering anything:

better check
Local checks only; the server validates the Design System again on push.
modules/testimonial: Add "testimonial" to modules.order; this module directory is unlisted.

Fix modules.order, run better check again, then open the preview and iterate on the Liquid:

better ds dev --open

When it looks right, confirm what you are about to send:

better ds diff
+ added Testimonial (cmp_testimonial)
~ Module order changed

Open a pull request. The check job runs better check against the branch; reviewers read the Liquid and the settings JSON as a normal diff. After merge:

better ds push --name "Testimonial module"
+ added Testimonial (cmp_testimonial)
~ Module order changed
Summary: 1 added, 0 edited, 0 removed, Module order changed.
Push this Revision? [y/N] y
Created Version 12: Testimonial module.
Staged as Candidate.

The module is now testable. Someone creates a campaign on the Candidate channel, checks the module in a real inbox, and publishes Version 12 from the app when it holds up. See candidate testing for the whole staging and publishing flow.

Manage in code

Once a design system's structure lives in a repository, an edit made in the app is a change nobody reviewed, and it puts the two out of sync: your next push is refused until you either pull it down or force past it. Turn on Manage in code in the design system's settings to stop that happening.

While it is on:

  • the in-app Module and template base editor is view-only, and Betty's design system chat is unavailable
  • the CLI and the public API are the only way to change modules, settings, and the template base
  • publishing a pushed version to Live stays a human action in the app, for Admins and Designers
  • campaign editing is completely unaffected

The editor shows an amber banner explaining why it is read-only. Turning the setting on requires an Admin, and is refused while someone holds a live editor seat, so nobody loses an unsaved draft.

Editor support

Every JSON file the CLI writes carries a $schema key, so any editor with JSON Schema support gives you completion, hover documentation, and inline validation with no configuration.

The Better Email Design Systems extension for VS Code adds what a schema cannot:

  • Liquid autocomplete. Typing in module.liquid completes the module's own settings unprefixed, template.* and global.* from the design system, and the context.* runtime values. Repeatable settings complete through the active for or tablerow loop variable, and image and link inputs offer the same sub-properties as the web editor. Hovering an input reference shows its name, type, and default value. Unsaved settings.json buffers win over the file on disk, so completion follows an input you just added.
  • Inline diagnostics. Saving a managed file runs better ds check --json and surfaces the findings in the Problems panel.
  • A module list in the activity bar, in modules.order, for jumping between modules.
  • Start and stop the local previewBetter Email: Start Local Preview runs better ds dev and opens the app-hosted preview, showing unsaved edits as you type. It opens in your system browser rather than an editor tab, because the app's sign-in cookies do not reach a VS Code webview.

Point betterEmail.cliPath at your better binary if it is not on the extension's PATH.

Command reference

CommandWhat it does
better login [--base-url URL] [--api-key KEY]Sign in through the browser, or store an organization API key.
better ds check [--json]Validate the local tree. No credentials, no binding. Also better check.
better ai-files install [--check]Write or refresh the agent teaching files. See working with AI agents.
better ds listList the organization's design systems with their Live, Candidate, and Latest version numbers.
better ds pull [ID] [--channel live|candidate|latest] [--dir DIR]Write a design system to files and bind the directory.
better ds dev [--port N] [--open]Local live preview with editable inputs.
better ds diff [--against live|candidate|latest]Compare the local tree with a channel. Exit 1 on changes.
better ds push [--name NAME] [--description TEXT] [--no-stage] [--force] [--yes]Create a version, staged as Candidate when allowed.
better ds statusBinding, remote version pointers, and clean or modified local state.