@simplrstudio/cli
v0.3.0
Published
Sync a SimplrStudio site to your editor: pull, push, publish.
Maintainers
Readme
@simplrstudio/cli
Sync a SimplrStudio site to your editor. Pull the site's files down, edit them in VS Code (or anything else), push them back, publish when ready.
simplr login --host https://your-server
simplr pull acme
cd acme
# …edit…
simplr push
simplr publishRequires Node 20 or newer.
Installing
npm install -g @simplrstudio/cli
simplr --helpA token and Node are the whole prerequisite list — nothing else has to be checked out or configured.
Prefer not to install globally? npx @simplrstudio/cli pull acme runs the same
command without putting anything on your PATH.
From inside Claude Code
The same package is a Claude Code plugin, so one install covers both:
/plugin marketplace add https://www.simplrstudio.com/claude-plugin/marketplace.json
/plugin install simplr@simplrstudioThat puts simplr on PATH, connects the theme authoring tools to the session,
and loads a skill describing both loops — so a session that has never heard of
this CLI can still be asked to publish a design. Log in once with
simplr login --host https://www.simplrstudio.com and every later session and
directory reads the same stored token.
Install it either way, not both: a global install and the plugin put two
simplr binaries on PATH, and which one wins is down to PATH order. They are the
same program, so the worst case is a version skew — simplr --version says
which you are running.
Upgrading
npm install -g @simplrstudio/cli@latestsimplr --version reports what you are running. If an upgrade ever changes the
workspace format, commands say so and name the fix rather than reading a
manifest they do not understand.
Getting a token
Tokens are per-user and are minted in the app under Settings → API tokens. The plaintext is shown once, at creation — it is stored only as a digest, so a lost token has to be replaced rather than recovered.
Supplying a token
Every command that talks to the server takes --token, and honours
SIMPLR_TOKEN — that is everything except login and logout, which manage the
stored token rather than using one. Nothing has to be logged in first, so a CI
job or a container can set the variable and run.
Precedence: --token → SIMPLR_TOKEN → the token stored for that host.
simplr login is the convenience for people at a terminal: it prompts for a
token, verifies it against the server, and stores it in ~/.simplr/config.json
with mode 0600 so later commands need no token at all. Tokens are stored per
host, so a staging and a production token can coexist.
SIMPLR_TOKEN is not per-host, so it is only used against a host something
actually named — a --host, SIMPLR_HOST, or a pulled site's manifest. If the
host would have come from stored config, the command errors instead of sending
the token somewhere you did not choose.
Choosing a server
Every command takes --host, and honours SIMPLR_HOST. Inside a pulled site
the host recorded at pull time is used, so day-to-day you never pass it.
Precedence: --host → SIMPLR_HOST → the pulled site's manifest → the host you
logged into first. There is no built-in default — the platform domain is
deployment-specific, and guessing it could point a push at the wrong server.
simplr status reports which host it actually reached and where both the host
and the token came from, when you need to know which of several won.
Local layout
acme/
.simplr/manifest.json # what the last sync saw — do not hand-edit
.claude/skills/authoring-sections/SKILL.md # the markup rules, for an agent session
conflicts/ # your version of any file a pull overwrote
docs/README.md # this document, refreshed on every pull
site/ # shared across every theme on the site
site/data/work.csv # a collection's rows — yes, site/ twice; see below
site_assets/logo.png # the media library
themes/
aurora/ # one directory per theme, its name slugged
pages/
home/
index.html # read-only: the page as its sections compose it
hero-41.html # a section — edit this
story-42.html
layout/
navigation-7.html # a layout partial — edit this too; see below
about/
index.html
hero-58.html # a different section from home's hero
styles/main.css
data/work.csv # a collection's rows as the theme ships them
data/work/41.csv # one section's own rows for that collection
metadata/pages/3.json # the app's own snapshots — leave these alone
metadata/components/41.json
settings/heading-typeface.json # the heading typeface picked in the editorEach page owns a directory, so a page's files move together. A section's file is named for its title plus a number the app assigns (a built-in's, by the app alone), and keeps that name if the section is retitled later — so two pages' "Hero" never share a file, and a file never moves out from under you.
index.html is the page assembled from its sections — it is there to show how
they compose, and pushing an edit to one is
refused, naming the section files to edit instead. Reordering is simplr page
add-section --at and simplr page replace. A successful push refreshes the
index.html of a page whose own section files it changed, so the composed view
keeps matching what you just pushed. A shared file — a layout/ partial, or data
under site/ — composes into pages the push has no way to name, so pull to see
those pages recomposed.
A layout/ partial is editable markup, exactly like a section — the nav,
the footer, anything every page shares. It sits in a layout/ directory under
the page that owns it, almost always pages/home/layout/: a page without a
layout of its own renders through home's, so an edit there reaches every such
page. A partial arrives rendered — real text, real links — rather than as the
template the site stores, and pushing it back updates the partial itself, so the
text you changed stays editable in the app afterwards. The one thing an edit
there cannot change is the navigation links: those are
built from the site's own pages, so a push that changes them says so and leaves
them alone — rename or reorder pages in the editor instead. Deleting a partial's file is
refused too; every page renders it, so remove it in the editor.
The directory a file sits in decides where it uploads. site/ and
themes/<theme>/ are separate namespaces on the server and may hold the same
path as genuinely different files. A file you create in your editor is routed by
where you put it, so put a new theme file inside the theme's directory; a file
already tracked keeps the destination the last pull recorded, which is how the
page directories can differ from the paths the server uses.
Inside site/, a file keeps the server's own path, and the server stores a
collection's rows at site/data/<field>.csv — so they land at
site/site/data/<field>.csv. Leave the doubling alone: a file moved to
site/data/ would push as a different file.
A collection's rows can sit at three layers, and a page shows the first it
finds: the site's own site/site/data/<field>.csv, then a section's own
themes/<theme>/data/<field>/<section-id>.csv, then the theme's shipped
themes/<theme>/data/<field>.csv. A theme's metadata/ and settings/ files
are written by the app for itself; leave them alone.
Files outside those two trees (notes, a README of your own) are listed by push
as ignored rather than uploaded. The directories the CLI writes for you —
conflicts/, docs/, .claude/ and .simplr/ — are passed over silently
instead, since they are not your doing. It is safe to git init a pulled site;
.git/ is skipped too.
Seeing your changes
push writes drafts, and drafts show up in the theme's preview immediately —
no publish step needed to see your own work.
pull prints a preview link for every theme, and push prints one for each theme
it just changed:
$ simplr push
uploaded themes/hackathon/pages/home/hero-41.html
1 uploaded, 0 deleted, 0 failed.
preview hackathon:
http://lvh.me:3000/accounts/5ed19a9c…/sites/1/themes/43/previewKeep that tab open — a reload picks up whatever you last pushed.
The site's public URL keeps serving the published version until you deploy, so you can push freely without touching what visitors see.
Publishing is usually done in the app — the theme gallery's Deploy changes
button, which reports how many unpublished changes it is about to promote.
simplr publish is the same operation for scripts and headless use.
Editor workflow
The CLI is editor-agnostic — a pulled site is just files, so any editor works:
simplr pull acme
code acme/ # or your editor of choice
# …edit…
simplr push # from the integrated terminalAnything outside site/ and themes/ is ignored by push, so editor config
(.vscode/), notes and your own README can live in the directory safely. .git/
is skipped too, so a pulled site can be version-controlled locally. push names
the first few files it ignored and counts the rest; simplr status lists them all.
There is no file watcher yet: v1 is edit, save, then simplr push by hand.
Commands
| Command | What it does |
| --- | --- |
| simplr login | Store an API token for a server |
| simplr logout | Forget a stored token (--all for every host) |
| simplr sites | List the sites your token can reach |
| simplr pull <site> | Download a site into ./<subdomain>/ |
| simplr pull | Refresh the workspace you are in, from its root or any directory below |
| simplr push | Upload files you changed |
| simplr publish | Make the current drafts live |
| simplr status | Compare local files against the server |
| simplr page create <file> | Build a new page from authored section HTML |
| simplr page add-section <file> | Add authored sections to an existing page |
| simplr page replace <file> | Replace every section on a page with the file's |
| simplr theme push [dir] | Publish a theme you authored, or update your own |
| simplr theme validate [dir] | Check a theme bundle without publishing it |
| simplr theme skill | Fetch the contract a theme bundle must satisfy |
| simplr theme list | List the themes you have published |
<site> accepts an id, a subdomain or a name.
push, publish, status and page act on the pulled site you are standing in:
its root, or anywhere under its site/ or themes/. Run from anywhere else
beneath a pulled site and they refuse, naming the site they found, rather than
guess that you meant it. Before writing, they print the site, server and
directory they resolved.
Every command below takes --host. Every command that acts on your behalf also
takes --token — that is, all of them except login, whose own --token stores
a token rather than using one once, and logout, which has no token to use. Both
are described under Supplying a token and
Choosing a server rather than repeated per command.
simplr <command> --help prints the options for one command.
login
Stores an API token for a server, so later commands need neither flag.
--token <token>stores this token instead of prompting for one. Its meaning differs from every other command's--token, which uses a token once without storing it.
logout
Forgets a stored token.
--allforgets every stored token, for every host, rather than just the one for the current server.
sites
Lists the sites the token can reach, with the id, subdomain and name that
pull accepts. No options of its own.
pull
Downloads the site and rewrites the manifest. Files you edited are not overwritten just because you pulled:
- Changed on the server, untouched here — downloaded.
- Edited here, unchanged on the server — your file is left alone. It stays a
pending change that the next
pushuploads. - Changed on both sides — you are asked, one file at a time.
Answering yes to a conflict takes the server's version and saves yours to
conflicts/<path> first, so nothing is discarded outright. Answering no keeps
your file as it is; simplr push --force then publishes it. The same question
covers a file deleted on the server that you edited, and a path created on both
sides.
conflicts/ sits at the top of the workspace, in plain sight — it mirrors the
layout of what it saved, so conflicts/themes/aurora/pages/home/hero-41.html holds your
version of that file. Nothing ever removes it; delete it yourself once you have
merged what you need. A second conflict on the same path replaces its backup.
Off a terminal (scripts, agents) there is nobody to ask, so conflicts are kept as-is, listed, and the command exits non-zero.
--dir <path>pulls into this directory instead of one named after the site's subdomain. The directory is created if it does not exist; pulling into an existing workspace reconciles against it as described above.--forcetakes the server's version everywhere without asking, discarding local changes. No backups are written andconflicts/stays empty, so an edit you had not pushed is gone. Prefer answering the per-file conflict prompts, which save your version before overwriting it.
push
Uploads created and modified files. Each upload carries If-Match, so if the
asset changed on the server since your last sync the write is refused rather
than silently overwriting someone else's edit:
index.html: changed on the server since your last sync — run `simplr pull` to reconcile, or `simplr push --force` to overwrite itA file that fails does not stop the rest. Successful uploads are recorded before
the command exits, so re-running push retries only what failed.
Pushing a built-in section converts it into an authored one. The section keeps its content and position, but it becomes a section you own outright: the editor no longer offers its variation switcher, and any field its owner left blank is gone, because a built-in's file is rendered from its current state rather than its full schema. The conversion is one-way.
It also renames the file. A built-in's file name is assigned by the app, while an
authored section takes the name its markup declares, so push moves the local
file to the new name and reports it:
uploaded themes/aurora/pages/home/page-1.html
→ now themes/aurora/pages/home/exclusions-63.htmlOnly files you have actually edited are uploaded, so pulling a site and pushing it straight back converts nothing.
--dry-runreports what the push would do, without doing it. See below.--forceuploads withoutIf-Match, overwriting whatever is on the server. This is how you publish a version you kept during apullconflict.--prunealso deletes assets whose local file is gone. Off by default: deleting an asset removes its published version too, so a strayrmwould take a live page down.--prunelists the paths and asks before deleting.--yesanswers the confirmation prompts — currently--prune's — with yes, for scripts and agents that have nobody to ask.
HTML is checked against the theme contract on write. A rejection prints the specific violations rather than a bare error.
Individual assets are capped at 10 MB.
What --dry-run checks
A dry run sends every file to the server as a real write, and the server runs
every check the push runs — then declines to commit. So it reports what the push
would actually answer, not a local guess at it: server-side drift, theme-contract
violations, refusals like pushing a page document or a section the site has no
component for, and the same compile warnings a real push prints. --dry-run
--prune rehearses the deletions the same way.
Nothing is written. No version, no draft, no published asset — including on a file that is refused partway through.
Two things it cannot tell you:
- A failure that happens only as the write is finalized. Storing the file's bytes and extracting CSV metadata both happen after the commit a dry run never reaches, so a failure confined to those steps is invisible to it.
- What someone else does next. A dry run reports the server as it is at that moment. If the file changes in the app's editor between the dry run and the push, the push can still be refused.
publish
With no arguments, publishes the drafts of the theme that is already live.
simplr publish --theme <dir> publishes a different theme — and that deploys
it: the named theme becomes what visitors see, and whatever was live is taken
down. The command spells this out and asks before proceeding. Use --yes to
skip the prompt in scripts.
Publishing changes the version of every asset on the site, so publish refreshes
your local manifest afterwards. Without that, the next push would report a
conflict on files you never touched.
status
Shows, in both directions: what you changed locally, what changed on the server,
what is missing on either side, and what push will ignore. No options of its
own — it only reports, and never writes.
page
Turns HTML you wrote — by hand or with an agent — into real, editable sections.
simplr page create sections.html --title "Our Work"
simplr page add-section hero.html --page our-work
simplr page add-section hero.html --page our-work --at 0
simplr page add-section hero.html --page our-work --replacing hero
simplr page replace pricing.html --page pricingThe file holds one or more sibling <section> elements and nothing else — no
<head>, no <html>. The site wraps them in its own layout, which already loads
the theme's fonts and defines its colours.
Each subcommand takes --theme <name>, which is required only when the
workspace holds more than one theme; with a single theme it is inferred.
This is deliberately not part of push, which still refuses to create a page
or a section from a file: a page and its collections are too much to bring into
being by dropping a file in a directory.
Every pull fetches .claude/skills/authoring-sections/SKILL.md from the server
and writes it into the workspace. It is generated from the same rules that server
validates against, so an agent session started here already knows what conformant
markup looks like — and it comes from the server you just talked to rather than
from whenever this CLI was installed. If it cannot be fetched, pull says so and
carries on; the ingest commands work without it.
A rejection names each violation with the rule that caught it. Warnings mean the markup imported but something in it could not be made editable — worth reading.
New pages are drafts; run simplr publish to make one visible.
page create
Creates a new page from a file of authored sections.
--title <title>required. The page title, which also derives its slug.--theme <name>the theme to add the page to. Required only if the workspace holds more than one.
page add-section
Adds the file's sections to a page that already exists.
--page <slug>required. The page to add to.--at <position>inserts at this position, counting from 0. Appends if omitted.--replacing <slug>swaps this section out while keeping its place. The original is soft-deleted, so it can be restored in the app.--theme <name>the theme the page belongs to. Required only if the workspace holds more than one.
page replace
Replaces every section on a page with the ones in the file. This is the verb to
reach for when you have edited a whole file and want the page to match it —
add-section --replacing swaps a single section and would leave the ones your
edit dropped still on the page.
Sections the file no longer declares are soft-deleted, so a replace you regret is undone in the app rather than re-authored. Markup that fails validation changes nothing at all — the page keeps the sections it had.
--page <slug>required. The page to replace.--theme <name>the theme the page belongs to. Required only if the workspace holds more than one.
theme
Publishes a theme you designed somewhere else — by hand, or in an agent session that never touched this repo — into your own private gallery.
simplr theme push
simplr theme push ./broadside --slug broadside
simplr theme validate ./broadside
simplr theme skill ./broadside
simplr theme listUnlike pull/push, there is no workspace and no manifest. Those exist to diff
per-asset state against a site; a theme you author has no site until you
install it on one, and publishing is a whole-bundle upsert with nothing to diff.
The directory is the unit, and its name is the slug unless --slug says
otherwise.
The bundle is the top-level *.html files in the directory, one of which must be
index.html — nested directories are ignored, because a theme's assets are
derived when it is packaged rather than uploaded. An optional theme.json
carries the metadata, and every key inside it is optional.
{ "persona": "gallery-owner", "design_language": "broadside", "tags": ["dark"] }An optional DESIGN.md beside it is sent as the theme's design language — the
ordinary push carries it, with no flag to remember. Bring one if you already
hold one; without it the theme publishes exactly as before, and the design
language is simply absent. The server parses it and refuses the whole submission
if it is malformed, naming the part that failed, so a broken file never lands a
theme that then answers every later edit in a house style you did not choose.
Publishing the same slug again updates your own theme in place rather than creating a second. Validation runs first and fails closed, so a bad republish leaves the live one exactly as it was. A theme we ship under the same name is untouched and stays untouched — yours is yours.
push prints a preview URL when it succeeds. That page is authorized by your
ordinary logged-in session, not by your token: open it in a browser you are
signed into. It renders the theme's home page only — push says so explicitly
when your bundle has pages the preview will not cover.
validate sends the same bundle to a route that writes nothing, and is the way
to find out what is wrong before anything of yours is replaced. It exits non-zero
and names each violation when the bundle fails.
theme push
Publishes the bundle, creating the theme or updating your own of that slug,
then prints what conversion changed or dropped, page and section, as warning:
lines.
--slug <slug>the name to publish under. Defaults to the directory's name.
theme skill
Writes .claude/skills/authoring-themes/SKILL.md into the directory: the contract
a theme bundle is confirmed against, fetched from the server rather than shipped
with this CLI. Run it before converting a design you already have, so the session
doing the conversion knows the bar rather than discovering it as refusals.
It is a different document from the one pull writes. That one teaches authoring
sections into a site that already exists; this one is the bar a whole bundle must
clear to publish at all.
theme validate
Checks the bundle against the theme contract and writes nothing. It reads the
same bundle push sends, so a DESIGN.md beside the theme is checked here too —
a malformed one is refused before you publish rather than after.
A bundle that passes can still lose something in conversion — a row style that
cannot be carried, an interaction the site cannot run. Each is printed as a
warning: line naming the page and section, and push prints the same list.
--slug <slug>the name to check under. Defaults to the directory's name.
theme list
Lists the themes you have published, with the preview URL for each.
mcp-headers
Prints {"Authorization": "Bearer …"} for the named server, reading the token
you are already logged in with. Claude Code runs this itself — the plugin's
.mcp.json names it as the helper supplying the MCP endpoint's headers — so
there is no reason to type it.
Being a helper rather than a report, it stays silent when no token is stored
instead of saying so: its stdout is parsed as the header map, and a sentence
would be parsed along with it. A session that finds the authoring tools missing
is one simplr login away from having them.
Source and bugs
The CLI is developed in the SimplrStudio repository, under cli/. Report a bug
at https://github.com/Ranauro-net/zcommerce/issues; the development setup is
in that repository's own README.
