CLI
@clicklesscms/cli brings every page, partial, and collection entry template onto disk as plain HTML — editable in any editor, scriptable from any LLM, and deployable with versioned writes, drift detection, and typed-domain confirmations. The CLI never triggers a production deploy; promotion stays in the dashboard.
Install #
$ npm install -g @clicklesscms/cli
$ clickless --version
That's it. There's no service-account key to ship, no API token to mint up-front — the CLI authenticates as a real user via your browser the first time you run a command.
Requirements #
- Node.js ≥ 18 — uses the built-in
fetchand modern crypto APIs. - An OS keychain (macOS Keychain, Windows Credential Vault, or libsecret on Linux) — used for credential storage. A
chmod 600file fallback is automatically used if the keychain backend isn't available. - A browser on the same machine for the first login. For SSH sessions, see Headless / CI below.
First login #
$ clickless login
Opening your browser to authorize the ClickLess CLI…
Listening on http://127.0.0.1:9005/callback (will time out in 120s)
Signed in as Carson Suite <[email protected]>
Session id: cli_abc12345
Credentials: OS keychain
The login flow uses PKCE (S256) plus a 256-bit CSRF nonce, a loopback-only listener on a free port in the 9005–9099 range, an Origin allowlist, and a hard 120-second timeout. See SECURITY.md for the full walkthrough.
Bind a folder to a site #
$ mkdir my-site && cd my-site
$ clickless init
Pick an organization:
1. The Crossing Church (org_abc123, owner, 3 sites)
2. Personal (org_def456, 1 site)
> 1
Pick a website in "The Crossing Church":
1. Main Site (ws_xyz789, thecrossing.church)
2. Donate (ws_abcdef)
3. Events (ws_ghijkl)
> 1
You're about to bind this folder to:
Organization: The Crossing Church
Site: Main Site
Domain: thecrossing.church
Preview URL: https://clickless.site/ws_xyz789
Write clickless.json here? [Y/n] y
Wrote /path/to/my-site/clickless.json
Created .gitignore (added: clickless.json, .clickless/).
Next: `clickless pull` to download pages, partials, and entry templates.
If you already know the org/site you want, skip the wizard: clickless use the-crossing-church/main-site.
First pull & deploy #
$ clickless pull
… downloading 47 files (pages, partials, entry templates, schemas, docs) …
Pull complete. 47 added, 0 conflicts.
$ vim pages/home.html # or open in your favorite editor / LLM
$ clickless status
On site: Main Site (thecrossing.church)
Pulled: 2m ago
Local changes (1 modified, 0 added, 0 archived):
M pages/home.html
Cloud changes since last pull: none.
$ clickless deploy --dry-run
… preview of what would change …
DRY RUN — no changes were sent.
$ clickless deploy
… typed-domain confirm + server write …
Deployment cli_dep_xyz committed.
Preview: https://clickless.site/ws_xyz789
Folder shape #
After clickless init and the first clickless pull, the project folder looks like this:
my-site/ # bound to ONE website via clickless.json
├── clickless.json # the project link
├── .clickless/ # CLI internal state (gitignored)
│ ├── snapshot/ # verbatim cloud-state copy at last pull
│ │ ├── pages/
│ │ ├── partials/
│ │ ├── collections/
│ │ ├── docs/
│ │ └── manifest.json # per-file hashes + cloud version ids
│ ├── lock # advisory file lock
│ └── audit.log # local timeline of commands
│
├── pages/ # deployable HTML pages
│ ├── home.html
│ ├── home.meta.json # read-only sidecar
│ ├── about.html
│ └── about.meta.json
│
├── partials/ # deployable HTML partials
│ ├── header.html
│ ├── header.meta.json
│ ├── footer.html
│ └── footer.meta.json
│
├── collections/ # one subfolder per collection
│ ├── staff/
│ │ ├── collection.meta.json # schema (LLM context)
│ │ ├── template.html # entry template (deployable)
│ │ └── template.meta.json
│ └── sermons/
│ ├── collection.meta.json
│ ├── template.html
│ └── template.meta.json
│
└── docs/ # canonical reference docs (auto-refreshed)
├── template-field-guide.md
└── clickless-cli-schema.md
Deployable vs read-only #
| Path pattern | Deployable? | Editable? |
|---|---|---|
pages/*.html, pages/**/*.html | Yes | Yes |
partials/*.html | Yes | Yes |
collections/*/template.html | Yes | Yes |
pages/*.meta.json, partials/*.meta.json, collections/*/template.meta.json | No | No (read-only in v1) |
collections/*/collection.meta.json | No | No (schema context) |
docs/** | No | No (auto-refreshed) |
clickless.json | No | No (managed by CLI) |
.clickless/** | No | No (internal state) |
| Any other file you add | No | Yes (CLI ignores it) |
If you edit a .meta.json locally and run deploy, the change-set will report No deployable changes. That's expected — metadata edits stay in the dashboard for v1.
clickless.json — the project link #
This file is the single source of truth for what cloud target the folder is bound to. Every mutating command re-verifies the triple { organizationId, websiteId, domain } against the live cloud doc.
{
"$schema": "https://clicklesscms.com/schemas/clickless.json/v1.json",
"organizationId": "org_abc123",
"organizationName": "The Crossing Church",
"websiteId": "ws_xyz789",
"websiteName": "Main Site",
"domain": "thecrossing.church",
"previewDomain": "clickless.site/ws_xyz789",
"lastPulledAt": "2026-05-21T16:42:11Z",
"lastPulledServerUpdatedAt": "2026-05-21T16:39:50Z",
"cloudContentHash": "sha256:7d2f…",
"files": {
"pages/home.html": { "hash": "sha256:…", "cloudVersionId": "v_…" }
},
"cliVersion": "0.1.0"
}
Don't hand-edit organizationId, websiteId, or domain. Every mutating command re-verifies the triple against the live cloud doc and aborts on mismatch with no --force override. To switch the folder to a different site, run clickless use <org>/<site> — the CLI will re-fetch from the cloud and overwrite the link safely.
Because committing this file across branches can drift the deploy target between branches, clickless init and clickless use automatically add it (and .clickless/) to your project's .gitignore if you're in a git repo.
.clickless/ — internal state #
| Path | Purpose |
|---|---|
.clickless/snapshot/ | Verbatim copy of the cloud state at last pull. Used for 3-way diff at deploy time. Do not edit. |
.clickless/snapshot/manifest.json | Per-file SHA-256 hashes and cloud version IDs for every pulled artifact. |
.clickless/snapshot/docs/ | Hashes of the bundled docs at last pull. The next pull compares against this to detect whether a CLI upgrade shipped a newer guide. |
.clickless/lock | Advisory file lock (PID + start time). Prevents two CLI commands from racing in the same folder. Stale > 10 min are auto-cleared. |
.clickless/audit.log | Local timeline of CLI commands run in this folder. |
Meta sidecars #
Every deployable file has a sibling *.meta.json sidecar carrying the cloud-side metadata. Sidecars are read-only locally in v1 — to change a title / slug / sortOrder, use the dashboard.
// pages/home.meta.json
{
"$readOnly": true,
"id": "page_xyz",
"slug": "home",
"title": "Home",
"status": "published",
"isHomePage": true,
"is404Page": false,
"sortOrder": 0,
"parentPageId": null,
"cloudVersionId": "v_abc"
}
Creating a new page: add pages/<name>.html with no .meta.json. On the next deploy, the server mints the new pageId + initial versionId and writes them into a fresh sidecar for you. The CLI never invents Firestore IDs.
"Deleting" a page: remove the .html file (and .meta.json if present). The next deploy sets the cloud doc's status to archived; the document and its versions are never hard-deleted. Recover via the dashboard or clickless versions.
Login flow #
The full PKCE + CSRF + loopback flow happens automatically when you run clickless login:
- CLI generates a 256-bit CSRF nonce and a 256-bit PKCE verifier; computes
challenge = base64url(sha256(verifier)). - CLI binds a single-shot HTTP listener on
127.0.0.1on a free port between 9005 and 9099. - CLI opens the user's browser to
console.clicklesscms.com/cli-loginwith the port, state, and PKCE challenge as query parameters. - The dashboard authenticates the user (via the existing Firebase Auth UI), calls
createCliSession, and POSTs the returned token back to the loopback listener. - CLI validates POST + path + Origin + state (constant-time) + body cap (≤ 16 KB), then exchanges the refresh token + PKCE verifier for a 15-minute Firebase custom token.
- Refresh token is persisted in the OS keychain (file fallback with
chmod 600); the listener shuts down within ~200 ms of success or after a hard 120-second timeout.
clickless whoami #
$ clickless whoami
Signed in as Carson Suite <[email protected]>
UID: f3aB2cD…
Session id: cli_abc12345 (5d old)
Credentials: macOS Keychain
Server: session is valid.
Use --offline to skip the server check (useful on flaky networks), or --json for a machine-readable envelope.
clickless sessions #
List and revoke active CLI sessions for your account:
$ clickless sessions
Active CLI sessions:
cli_abc12345 * active created 5d ago used 2m ago macOS Terminal
cli_def67890 active created 12d ago used 1d ago GitHub Actions (CI)
cli_ghi98765 active created 28d ago used 14d ago Ubuntu / xterm
`*` is the session this CLI is currently using.
$ clickless sessions revoke cli_ghi98765
Revoked session cli_ghi98765.
clickless logout #
clickless logout revokes the current session server-side, then removes local credentials from keychain and the file fallback. Subsequent commands will prompt to log in again.
Headless / CI #
For CI runners, SSH sessions, and other browser-less environments, mint a long-lived CI token from a workstation that does have a browser:
$ clickless login:ci
Minted CI session cli_abc12345 for [email protected].
Save this token in your CI's secret store, then export it as $CLICKLESS_TOKEN:
export CLICKLESS_TOKEN="cli_abc12345:RrTk…3pQ8"
Then in your CI script, before running clickless:
export CLICKLESS_AUTOMATION=1
clickless deploy --force
This token is valid for 90 days. Rotate via `clickless sessions revoke cli_abc12345`
+ a fresh `clickless login:ci`.
CI sessions are tagged with a (clickless-ci) userAgent marker — they show up in clickless sessions with a (CI) suffix so you can spot them and revoke them quickly.
Store $CLICKLESS_TOKEN in your CI's secret manager — never echo it to logs. If you suspect a token leak, revoke the session immediately via clickless sessions revoke <id> or the dashboard's "Connected CLIs" panel.
clickless pull — overview #
Downloads the active versions of every page, partial, and collection entry template for the bound site. Also refreshes collection schemas (read-only context) and bundled reference docs. Maintains .clickless/snapshot/ for 3-way diffs at deploy time.
Pull is idempotent. Running it twice in a row with no cloud changes is a no-op; running it after only local edits leaves your edits in place (see conflict handling below).
Conflict handling #
If both you and the cloud have changed the same file since the last pull:
pages/home.html.local— your working-tree versionpages/home.html.cloud— the current cloud versionpages/home.html.base— the snapshot from the previous pull
The live pages/home.html is left untouched. Resolve in your editor or with an LLM, delete the three .local/.cloud/.base files, then re-run clickless pull to refresh the snapshot. Pull exits non-zero on any conflict so CI catches it.
Bundled docs refresh #
Two canonical reference documents ship with the CLI itself:
docs/template-field-guide.md— full templating syntax referencedocs/clickless-cli-schema.md— canonical local-folder schema reference
On every pull, if the bundled hash differs from the local hash, the local copy is overwritten with a one-line banner. Locally edited copies are silently overwritten — users wanting to fork a guide should keep their fork outside docs/.
clickless deploy — overview #
Pushes local changes to the bound site as new Firestore version documents, then triggers a preview build at clickless.site/{websiteId}. Never triggers a production deploy.
The flow runs every safety check in this order; any failure aborts before any write:
- Auth check + project-link parse.
--forcegate (if requested) — must be allowed by config or env var.- Folder lock acquired.
- Triple-identifier verification against the live cloud doc.
- Per-artifact ownership check (every modified/archived id must still belong to this website).
- Cloud-bundle hash recomputed; drift surfaced if it doesn't match the snapshot.
- Local change-set built from
.clickless/snapshot/. - Typed-domain confirmation prompt (and
ARCHIVEtoken if archiving any artifact). - Server callable invoked with the validated payload.
- Per-artifact results written back into local sidecars + snapshot.
- Preview build triggered.
Dry-run #
clickless deploy --dry-run runs every step above through #7 (change-set summary) and then exits with a clear "DRY RUN — no changes were sent" footer. No server callable is invoked. --dry-run and --force are mutually exclusive.
Drift handling #
If the cloud has changed since your last pull:
!! Cloud has changed since the last `clickless pull`.
Local snapshot hash: sha256:7d2f4a…
Current cloud hash: sha256:a91f3b…
Files changed on the cloud since your last pull:
M pages/about.html
M partials/footer.html
+ pages/contact.html
What would you like to do?
[P]ull cloud changes first (recommended)
[F]orce deploy and overwrite (creates new versions)
[C]ancel
The [F]orce path requires --force AND the automation gate (see below). In non-interactive shells without --force, drift hard-aborts with CLOUD_DRIFT_DETECTED.
Confirmations #
Before any server write, deploy demands you type the site's domain back verbatim:
You are about to deploy to PREVIEW:
Organization: The Crossing Church
Site: Main Site
Site domain: thecrossing.church
Preview URL: https://clickless.site/ws_xyz789
To confirm, type the site domain exactly:
> thecrossing.church
Matching is case-insensitive and whitespace-trimmed. Partial matches are rejected.
If the change-set includes any archives (deleted local files), deploy requires a second typed token: literally the string ARCHIVE.
Force gating #
--force bypasses the drift prompt AND the typed-domain confirmation — both. Because that's a lot of safety to skip, it's gated:
$ clickless config set automation.allow-force true # persistent
# OR
$ CLICKLESS_AUTOMATION=1 clickless deploy --force # per-command
Without one of those signals, --force in an interactive shell fails fast with FORCE_NOT_ALLOWED. This makes habit-typed --force a clean error rather than a silent overwrite.
Partial success #
Each artifact in a deploy is its own atomic batch on the server. If one fails (e.g. a permissions issue on a single page), the other artifacts continue and are committed. The CLI prints a per-artifact failure list and the server records the full breakdown in the cliDeployments audit doc:
Deploying…
1 artifact failed to deploy:
x page page_xyz (update) — permission-denied
The audit doc captures full details; the rest of the deploy committed.
Deployment cli_dep_abc12345 committed.
clickless status #
Git-style summary of local changes plus drift between the snapshot and the live cloud:
$ clickless status
On site: Main Site (thecrossing.church)
Pulled: 3h ago
Local changes (2 modified, 0 added, 0 archived):
M pages/home.html
M partials/footer.html
Cloud changes since last pull (1 modified):
M pages/about.html
Run `clickless diff` to inspect local changes, then `clickless deploy`.
Cloud has moved since the last pull. Run `clickless pull` to refresh.
--offline skips the cloud round-trip. --json emits the full structured changeset.
clickless diff #
Unified diff against the snapshot, or against the live cloud version with --cloud:
$ clickless diff pages/home.html
$ clickless diff pages/home.html --cloud
$ clickless diff # diff every modified file (cap 25)
If $PAGER is set and stdout is a TTY, output is piped through it. --no-pager disables.
clickless versions #
List the version history of a single artifact:
$ clickless versions pages/home.html --limit 5
pages/home.html → pages/page_xyz
Currently active: v_abc123
v_abc123 * 2026-05-21 09:14 [email protected] cli "Deploy from CLI session cli_xyz789"
v_def456 2026-05-20 16:39 [email protected] dashboard "Hero copy tweak"
v_ghi789 2026-05-20 11:22 [email protected] ai-editor "Make the headline more punchy"
v_jkl012 2026-05-18 14:01 [email protected] dashboard "Initial setup"
v_mno345 2026-05-15 09:00 [email protected] migration "Migrated from legacy content field"
Showing 5 versions. Use --limit to see more.
For artifacts that no longer exist locally (e.g. archived pages), pass --kind page --id <cloudId> instead of a path.
clickless restore #
Pull a specific historical version back to your working tree. Does not deploy — it's a working-tree change only. Run clickless deploy afterwards to make the restored version active in cloud.
$ clickless restore pages/home.html --version v_def456
Overwrite pages/home.html with version v_def456? (Working-tree change only.) [y/N] y
Restored pages/home.html to v_def456.
This is a working-tree change only. Run `clickless deploy` to make it active in cloud.
clickless deployments #
Recent CLI deploys for the bound site:
$ clickless deployments --limit 5
Recent CLI deploys for thecrossing.church:
2026-05-21 09:14 cli_dep_abc12345 pages:3 partials:1 templates:0 sha256:7d2f4a→sha256:a91f3b
2026-05-20 16:42 cli_dep_def67890 pages:1 partials:0 templates:0 sha256:a91f3b→sha256:3b4c1d (force)
2026-05-19 11:30 cli_dep_ghi98765 pages:0 partials:2 templates:1 sha256:3b4c1d→sha256:f2e9a0
…
For full per-artifact detail, see the dashboard's "Recent CLI deployments" panel.
clickless config #
$ clickless config list
$ clickless config get automation.allow-force
$ clickless config set automation.allow-force true
The config file lives at ~/.config/clickless/config.json (or %APPDATA%\clickless\config.json on Windows; honors $XDG_CONFIG_HOME). It carries automation.allow-force, ui.color, and a few other knobs.
clickless open #
Open the dashboard for the active site in your default browser. --print-url skips the spawn step for SSH sessions and just prints the URL.
clickless preview #
Open the preview URL (clickless.site/{websiteId}) for the active site. Useful right after a deploy.
clickless doctor #
One-shot health check. Run this whenever something feels off — it's the first thing support will ask you for.
$ clickless doctor
PASS Auth: signed in as [email protected]
PASS Project link: 1 (thecrossing.church)
PASS Triple-identifier check: org / website / domain all match cloud
PASS Snapshot integrity: 47 of 47 files match their recorded hash
PASS No nested project link in parent directories
WARN Metadata edits detected (pages/home.meta.json edited 2h ago); meta is read-only — use the dashboard
PASS CLI 0.1.0 (latest on npm: 0.1.0)
PASS Network reachable (https://console.clicklesscms.com responded)
doctor: 7 pass / 1 warn / 0 fail.
JSON envelope #
Every read command (projects, status, diff, versions, restore, deployments, sessions, doctor, config, open, preview, whoami, login:ci) supports --json and emits the canonical envelope on both success and failure paths:
// success
{ "ok": true, "data": { /* command-specific payload */ } }
// failure
{ "ok": false, "error": { "code": "FORCE_NOT_ALLOWED", "message": "…" } }
This makes the CLI safe to script against — branch once on ok, no per-command special-casing.
Tenant isolation #
The CLI enforces tenant isolation end-to-end. Every project folder is bound to a single website, every mutating command re-verifies that binding against the live cloud doc, and every server callable re-checks ownership independently. Highlights:
- One folder = one website. The
deployArtifactsFromClicallable's payload schema accepts a singlewebsiteIdstring. Multi-site batch deploys are structurally impossible. - Triple-identifier check (org id, website id, domain) on every mutating command. Re-fetched fresh from the cloud each time.
- Per-artifact ownership check — every modified / archived artifact's cloud doc must still belong to the bound website.
- Server-side drift gate — the server independently recomputes the bundle hash and rejects on mismatch unless
force === true. - No production deploy from the CLI, ever. See below.
Typed-domain confirmation #
Before any server write, you have to type the site's domain back exactly. Case-insensitive, whitespace-trimmed, no partial matches. The only way to skip is --force with automation enabled.
ARCHIVE second-token #
If your change-set includes any deleted local files (which become status: 'archived' on the cloud doc), deploy demands a second typed token: literally ARCHIVE. This makes accidental rm or branch-checkout-induced deletions a non-issue.
No production deploy #
The CLI cannot trigger a production deploy. There's no code path for it; a CI grep test fails the build if anyone tries to import one. Promotion to production stays a dashboard-only action — by construction, not by convention.
Versioned writes #
Every CLI deploy creates new version documents in Firestore — old versions are never deleted. Archives flip status: 'archived' rather than hard-deleting. The shared versionedWrite helper makes the parent-doc update + version-doc write atomic via a single Firestore batch, populates an explicit activeVersionId pointer, and migrates legacy pre-versioning docs on first write.
Even a --force deploy is non-destructive at the data layer — you can roll back any artifact via clickless versions + clickless restore.
Doctor pass / warn / fail #
The 8 doctor checks classify their findings as PASS / WARN / FAIL. The command exits non-zero iff any FAIL surfaces, so CI scripts can clickless doctor as a precondition for clickless deploy.
Common patterns:
FAILon the triple-identifier check usually means the site was moved between orgs since your last pull. Re-runclickless init.WARNon metadata-drift means you edited a.meta.json; those edits are read-only in v1 and won't deploy — use the dashboard.WARNon snapshot integrity (a file hash drifted on disk without you editing it) suggests a partial or interrupted pull; re-runclickless pull.
Revoke a session #
If a CI token leaks, or a laptop is lost, revoke the session immediately. Two channels:
- From any logged-in CLI:
clickless sessions revoke <sessionId> - From the dashboard: Settings → Connected CLIs → Revoke.
Revocation is server-side immediate. The next call from that session returns unauthenticated and the CLI prompts to log in again.
FORCE_NOT_ALLOWED #
You passed --force without enabling the automation gate. Fix:
$ clickless config set automation.allow-force true # persistent
# OR (for one-off CI invocations)
$ CLICKLESS_AUTOMATION=1 clickless deploy --force
CLOUD_DRIFT_DETECTED #
Someone (or another writer) changed the site between your last pull and this deploy. The safe path is to pull and re-deploy:
$ clickless pull # may surface .local/.cloud/.base conflict files
$ clickless deploy
If you're certain you want to overwrite the cloud (the new versions stay recoverable), pass --force.