Now with AI-powered page building via MCP Server

The cmssy CLI

init generates the wiring into an existing app, add block scaffolds and registers a block, link connects the app to a workspace and proves the wiring works.

Three commands, each for a moment that used to be hand-work: wiring a new app, adding a block, and connecting to a workspace.

cmssy init

cmssy never scaffolds your app. The framework is an adapter, never the foundation - you create the app with Next.js's own CLI, and init adds the cmssy wiring to it.

npx create-next-app@latest my-site
cd my-site
npx @cmssy/cli init

The CLI targets Next.js with the App Router and needs Node 18.18+. Astro, Remix and React Router are supported by the SDK itself - you wire those by hand rather than through init.

It is idempotent: a file that already exists is skipped and reported as skipped, never overwritten. Run it twice and the second run changes nothing.

What it writes

cmssy.config.ts
proxy.ts
next.config.mjs
env.example
cmssy/blocks.ts          block registry
cmssy/editor.tsx         editor bridge
blocks/hero/block.ts
blocks/hero/Hero.tsx
blocks/hero/Hero.module.css
app/[[...path]]/page.tsx catch-all route
app/api/draft/route.ts   draft preview

Notice what is not there: no app/sitemap.ts, no app/robots.ts, no /cmssy-edit route, no root layout, no editable layout.

That is deliberate rather than missing. Those files encode decisions about your app - which URLs you expose, what your chrome looks like, how you shape a sitemap - and a template that guessed them would be wrong for most projects. init gives you a rendering page and a draft route; the rest you add as you need it, following routes and pages, layouts and SEO.

Flags: --dir <path> targets an app outside the working directory, --force overwrites existing wiring.

cmssy add block

Every block after the generated hero used to mean hand-copying files and remembering the registry edit.

cmssy add block pricing-table
cmssy add block faq-list --dir ../my-site

Names are all derived from the kebab-case argument: pricing-table becomes type pricing-table, label Pricing Table, component PricingTable, export pricingTableBlock.

It writes the block files and registers them in cmssy/blocks.ts - adding the import and appending to the blocks array, preserving your formatting and existing entries.

It refuses to touch anything ambiguous: an invalid name, an already-registered block, existing files, or a registry without an export const blocks = [...] array. Each is a loud failure with the manual step spelled out - never a silent partial write.

The generated block starts with a required heading and an optional text. Edit the props and markup, restart the dev server, and the editor picks up the new type from the manifest handshake.

Connecting an app to a workspace used to mean hand-copying five values between the dashboard and .env.local - and the editor stayed dead until every one was right.

npx @cmssy/cli link
cmssy link --token cs_... --workspace acme/shop --preview-url https://shop.example.com

It authenticates with an API token (from --token or CMSSY_API_TOKEN; .env.local and .env are read first, never overwriting shell variables), picks a workspace, reads the draft secret, writes CMSSY_ORG_SLUG, CMSSY_WORKSPACE_SLUG and CMSSY_DRAFT_SECRET into .env.local - merging, so existing lines and comments survive - then runs the preflight and prints the editor deep link.

Reading the draft secret needs the PAGES_EDIT permission. A missing permission is reported as exactly that.

The preview URL is shared - localhost is rejected

--preview-url sets the origin the editor frames your app at for everyone in the workspace. A localhost value is rejected on purpose: pointing the shared preview at your machine would break the editor for every colleague.

For local development, toggle dev mode in the cmssy editor and enter your local host there. That target is per user and touches nothing shared. Without the flag the value is left unchanged.

The checks

  • Workspace reachable - public.siteConfig answers for the linked org and workspace. Distinguishes wrong slugs, network problems, and a workspace over its delivery limit.
  • Draft secret - the backend confirms the written secret matches. On a platform without the field yet it reports ? and continues.
  • Editor deep link - always printed.
  • Draft preview link - printed when the workspace reports a preview URL. Exit draft mode with /api/draft?disable=1.

Statuses are three, not two: verified, broken with the fix on the next line and exit code 1, and ? could not be verified - which never blocks. That third state exists because "unknown" and "broken" are different, and a tool that collapses them either cries wolf or hides a real failure.

The preflight is also an API

Every check is a pure function in @cmssy/core, exposed under a subpath so dev tooling never enters your production bundle:

import {
  checkWorkspaceReachable,
  checkDraftSecret,
  checkPreviewUrl,
  checkFrameAncestors,
  buildEditorUrl,
} from "@cmssy/core/preflight";

Each returns { status: "ok" | "fail" | "unknown", message, fix? }. The first two talk to the delivery API; the rest are pure string logic. None import a framework or a Node built-in, so they run anywhere - a health endpoint, a CI step, a dashboard.

When wiring is broken

In development the same checks guard the edit route. An editor request that fails verification does not 404 - the adapter renders a diagnostics page inside the editor iframe, one line per check: missing env vars and where to get them, an unreachable workspace, a draft secret mismatch, the preview URL comparison, and the origins frame-ancestors must allow.

It shows the workspace slug and which check failed, never a secret value.

Next steps