Local development CLI for Primo - build and edit sites with a visual CMS.
npm install -g primo-cli# Create a new site
primo new my-site
# This starts the local CMS automatically
# It scaffolds a workspace like:
# ./server.yaml
# ./library/
# ./sites/my-site/Initialize a new Primo workspace (server) in a new folder or the current directory.
primo init # Initialize in the current directory
primo init my-workspace # Create and initialize "my-workspace"Create a new site with starter files.
primo new # Interactive prompt for name
primo new my-site # Create "sites/my-site" in the current workspace
primo new --skip-dev # Create files without starting CMSStart the local CMS server. Watches for file changes and syncs edits from the CMS back to local files.
primo dev # Start in current directory
primo dev -p 8080 # Use custom port
primo dev --author files # Push file edits to the CMS; CMS UI is read-only (default)
primo dev --author cms # CMS edits write to files; file edits revert
primo dev --author both # Experimental bidirectional sync; conflicting edits can be overwritten--author both is experimental: conflicting edits can overwrite local files or CMS changes. Commit or back up your workspace before using it. Prefer --author files or --author cms, using one authoring mode at a time. In CMS mode, local files mirror the CMS, so make file edits in files mode instead.
The port comes from --port, then port: in the workspace's server.yaml, then 3000. Primo also reserves the next port for reload. If the default pair is occupied, it automatically chooses the next available pair and prints the URL. If you explicitly set a port, Primo asks before using another pair for the session; noninteractive runs fail with instructions to pass another --port.
The selected port is saved in .primo/dev-server.json, leaving server.yaml unchanged. Status, previews (CLI and MCP), new-site handling, and local pull discovery use this session record. A second dev server for the same workspace is refused to avoid opening its database twice. --force explicitly stops processes on the requested ports; automatic fallback never stops them.
Save local changes to the editable hosted draft. Plain push does not publish
them to the public website. Requires a server you've
already deployed (run primo deploy first) and authenticated against (primo login -s <server-url>).
primo push https://cms.example.com --site abc123
primo push --only my-site # Save one hosted draft (workspace root)
primo push --only my-site --publish # Save the draft, then publish it
primo push --json # Separate push/publication results for scripts
primo push --preview # Preview changes without applying
primo push --dry-run # Show what would be sent without making requestsOptions:
-s, --server <url>- Server URL--site <id>- Site ID--only <slug>- Push only the named site folder undersites/(skips library)-d, --dir <dir>- Directory (default:.)-t, --token <token>- Auth token--publish- Publish selected sites after all site/library imports succeed--json- One machine-readable result on stdout; diagnostics go to stderr--preview- Preview the import without applying or publishing--dry-run- Show what would be pushed without sending requests--force- Intentionally overwrite server changes after confirmation, with a backup--yes- Confirm--forcewithout an interactive prompt
Pull saves a revision for each site and the shared library in .primo/sync-state.json, scoped to the source server. A normal push stops if that server data changed since the last successful pull or push. An existing site without a baseline is also blocked. Update both the CMS and CLI to use this protocol; an older server cannot be bypassed with --force.
From a workspace root, every included site and the library are checked before the first upload. Each import checks again before writing. If a client edits a later site during the push, earlier successful imports remain saved; the CLI stops and lists completed, failed, and unattempted targets. --only <slug> checks and pushes only that site.
Save your local work before pulling after a conflict. Pull is not a merge and may replace local files. No automatic pull, retry, or content merge happens on a conflict.
To intentionally replace server data with your local files:
primo push --force # Lists targets and asks for confirmation
primo push --only my-site --force # Overwrite one site
primo push --force --yes # Explicit confirmation for scriptsBefore each overwrite, the server saves a ZIP export under pb_data/push_backups/. If backup creation fails, that import is rejected. The CLI prints an authenticated download URL and saves a copy under the target's .primo/backups/ directory. Server backups are retained until an operator removes them. A new edit after preflight/confirmation still stops a forced push. The ZIP also includes original records in .primo/backup-records.json for operator-assisted recovery of properties the portable importer cannot yet round-trip.
To recover content, extract the backup into a separate directory, set the intended server in its site.yaml, review it, then push that directory with --force. For a library backup, extract into a separate workspace and use primo library push <server> --dir <workspace> --force. Recovery creates another backup before overwriting. Push changes CMS data; publishing the website remains a separate action.
primo library push supports the same --force and --yes options. Local primo dev watcher imports retain their author-mode behavior; explicit primo push requests are always checked.
Pull an entire hosted Primo server — all sites plus the shared library — to local files.
primo pull https://cms.example.com
primo pull https://cms.example.com -o ./my-workspaceOptions:
-s, --server <url>- Server URL (auto-detects local)-o, --output <dir>- Output directory (defaults to./<server-hostname>)-t, --token <token>- Auth token
Pull the shared block library into a workspace root.
primo library pull https://cms.example.com
primo library pull -o ./my-workspaceOptions:
-s, --server <url>- Server URL (auto-detects local)-o, --output <dir>- Workspace output directory (default:.)-t, --token <token>- Auth token
Publish the current hosted draft, including edits made in the CMS, without
uploading local files. Use the same server, token, --dir, --site, and --only
selection options as push. From a workspace root it publishes each included
site; it does not publish the shared library separately.
primo publish --only my-site
primo publish https://cms.example.com --site abc123 # No local checkout required
primo publish --dir sites/my-site --json
primo push --only my-site --publish --json
primo status --hosted --only my-site --jsondeploy provisions hosting, push saves drafts, publish makes hosted drafts
public, and preview rebuilds a local primo dev preview. --publish cannot be
combined with --preview or --dry-run. Publication uses your hosted login,
never the local dev-auth endpoint.
A combined push waits for all selected imports (including the shared library)
before publishing any sites. If an import fails, publication is not attempted;
earlier draft imports remain saved. Publications then run per site, with every
outcome reported separately. A failed publication exits nonzero, preserves the
successful push baseline and draft, and prints a scoped primo publish retry
command. Retry publication without re-uploading the successful push.
JSON results include ok, results, and per-target push/publish objects
with state, revisions, attempt IDs, and stable error codes. States distinguish
not_requested, not_attempted, succeeded, failed, and unknown. A lost
publication response is checked against server status; if it cannot be resolved,
the outcome is unknown. Check status before retrying. Tokens are excluded from
results and retry commands.
status --hosted (also implied by --server) reads authenticated server state:
draft and published revisions, unpublished changes, last publication and attempt,
errors, and the public URL. It reports never_published, current, behind,
publishing, failed, or unknown. Local status remains available without
hosted authentication. Legacy publications have an unknown revision; unavailable
servers yield unknown status and a nonzero exit, rather than cached success.
This workflow requires the accompanying CMS publication endpoints and migration. Upgrade the CMS and CLI together and pull to refresh push baselines: publication artifacts are now excluded from the content fingerprint. Builds activate only after generation succeeds and the draft revision is checked again. A concurrent CMS edit rejects publication; review it before retrying. Failed builds retain the previous public output. Active and previous tracked builds are retained; older tracked builds and failed staging files are cleaned up best-effort.
Push the local shared block library back to a hosted Primo instance.
primo library push https://cms.example.com
primo library push https://cms.example.com -d ./my-workspaceOptions:
-s, --server <url>- Server URL-d, --dir <dir>- Workspace directory containinglibrary/(default:.)-t, --token <token>- Auth token
Authenticate with a hosted Primo instance.
primo login https://cms.example.com
primo login https://cms.example.com -e user@example.comDeploy the entire workspace — all sites under sites/, plus library/ and
server.yaml — as one editable-CMS unit, to Railway or Fly.io. Must be run
from the workspace root (the directory containing server.yaml).
primo deploy # Interactive provider selection
primo deploy -p railway # Deploy to Railway
primo deploy -p fly # Deploy to Fly.io
primo deploy --dry-run # Show what would be deployed without doing anythingFor other hosts (Netlify, Vercel, Cloudflare, GitHub Pages), use primo build
on a single site and deploy the output folder with that host's CLI.
| You want to… | Use |
|---|---|
| Let collaborators edit content from a CMS UI | primo deploy |
| Ship a static site to any static host | primo build |
| Save local edits to a hosted draft | primo push |
| Publish the current hosted draft | primo publish |
| Save local edits and publish them | primo push --publish |
Check site structure for errors.
primo validate
primo validate --strict # Strict modeBuild static HTML site for deployment to any static host.
primo build # Output to ./dist
primo build -o ./public # Custom output directoryDeploy the output anywhere:
# Netlify
npx netlify deploy --prod --dir=dist
# Vercel
npx vercel dist
# Cloudflare Pages
npx wrangler pages deploy dist
# Or just push to a repo connected to any static hostGive an MCP-capable coding agent (Claude Code, Claude Desktop, Cursor, VS Code / Copilot, Codex,
OpenCode, Gemini CLI, Cline, Windsurf, Continue, Zed) direct access to Primo
tools. The server is the official primo-mcp
stdio server; install it globally for the direct command, or let the CLI fall
back to npx -y primo-mcp.
npm install -g primo-mcp # recommended prerequisite
primo mcp install # detect clients and merge the Primo entry
primo mcp install --dry-run # show the target paths and what would change
primo mcp install --client cursor --client vscode
primo mcp install --all --global
primo mcp list # what's detected, configured, and where
primo mcp print --client opencode # paste it yourselfinstall merges only the primo entry: existing settings, sibling servers and
comments are preserved. Every write backs up the file first
(<file>.bak-<timestamp>) and is atomic. Re-running is idempotent, --dry-run
writes nothing, and a conflicting primo entry is left in place unless you pass
--force. Files with JSONC comments that can't be preserved are never rewritten
— primo mcp print emits the snippet instead.
MCP is optional. primo validate and primo build work without it.
workspace/
├── server.yaml
├── library/
└── sites/
└── my-site/
├── site.yaml
├── blocks/
├── pages/
├── page-types/
└── site/
Run primo dev from a workspace folder to work on multiple sites at once:
workspace/
├── server.yaml # Optional: port + site_groups
└── sites/
├── site-one/
│ └── site.yaml # includes group: default
└── site-two/
└── site.yaml # includes group: default
Each site gets its own subdomain: site-one.localhost:3000, site-two.localhost:3000
The shared block library can live at the workspace root alongside the sites/ folder:
workspace/
├── server.yaml
├── library/
│ ├── marketing/
│ │ └── hero/
│ └── shared/
│ └── footer/
└── sites/
├── site-one/
└── site-two/
Full documentation: docs.primo.build
- Node.js 18+
- For
primo deploy: Railway CLI or Fly.io CLI