@desenlabs/cli
v1.0.0-beta.2
Published
DESEN Protocol CLI — daemon server with REST API, WebSocket, governance, and canary rollout
Maintainers
Readme
@desenlabs/cli
The DESEN Protocol CLI + daemon. Provides
validate,audit, anddaemoncommands — plus the governance server behind them. Usually consumed throughdesen.
When to install this directly
Most users should install desen, which bundles this CLI. Reach for @desenlabs/cli only when you are:
- Embedding the daemon as a library inside your own tooling.
- Forking a command and don't want the full
desenumbrella. - Running in a container where you need a smaller footprint and no
@desenlabs/react-renderertransitive.
Commands
# Validate a document against the protocol schemas (§2)
desen validate <path>
# Classify and record governance outcomes (§5.2.1 + §5.4)
desen audit <path>
# Generate self-contained static HTML for every surface in a workspace
desen preview build [workspace] [-o out] [--theme <id>] [--max-bytes <n>] [--fail-if-not-empty]
# Start the local daemon — HTTP + WebSocket
desen daemon [-p <port>] [--log-format=<json|human>]Static export (desen preview build)
For documentation screenshots, embed-anywhere demos, and CDN deploys, desen preview build walks <workspace>/surfaces/*.json, applies the chosen theme, and writes one self-contained HTML file per surface to out/:
desen preview build examples/starter-template/desen-workspace
# [preview build] workspace=…/desen-workspace
# out=…/out
# theme=default
#
# welcome 2.1 KB 7 nodes
#
# [preview build] 1 surface(s), 2.1 KB total| Flag | Default | Behaviour |
|------|---------|-----------|
| <workspace> (positional) | desen-workspace | Workspace root containing surfaces/ and themes/ |
| -o, --out <dir> | out | Output directory (created if missing) |
| --theme <id> | default (or first available) | Theme id — file basename without .json |
| --max-bytes <n> | (unlimited) | Fail the build if any single output exceeds this many bytes (pre-gzip). Issue #41 sets the budget at 200 000. |
| --fail-if-not-empty | off | Refuse to overwrite a non-empty out/ directory (CI hygiene) |
What you get per surface:
- A complete HTML5 document —
<!doctype html>, viewport meta, a tiny<style>reset. - All
*_tokenreferences resolved against the chosen theme to literal CSS values. - §3.4 fail-closed guardrails enforced (depth ≤ 30, repeater items ≤ 100).
- §3.3 visibility fail-closed honoured — bound subtrees that don't resolve render nothing.
- A11y constraints (
aria_role,aria_label) applied to interactive elements. - A
<meta name="x-desen-surface-id">tag so downstream tooling can identify the source.
The output is intentionally dependency-free: no React, no JS bundle, no hydration. Drop the file into any web server (or file://) and it renders.
Schema validation runs first — invalid surfaces fail the build with a per-issue report, the same way
desen validatedoes.
Hot-reload
The daemon watches <workspace>/.desen/governance.json and <workspace>/.desen/proposals.json for out-of-band edits — opening either file in your editor, having a CLI workflow rewrite it, or committing a new revision via git all reflect into the running daemon without a restart.
| File | What gets reloaded | WebSocket event |
|-------------------|-------------------------------------------------------|-----------------------|
| governance.json | All future governance evaluations re-read from disk | governance_reloaded |
| proposals.json | The in-memory proposal list is swapped | proposals_reloaded |
Behaviour:
- Debounced — bursts of writes (e.g. an editor's atomic-rename save) collapse into one reload after ~300 ms.
- Fail-safe — if the new file is malformed JSON, the daemon keeps the previous in-memory state and emits a
governance_reload_failed/proposals_reload_failedlog line at error level. The daemon never crashes on a bad edit. - Broadcast — connected Console / Preview clients receive the event over WebSocket and refresh their views automatically.
Logging
The daemon emits structured logs. By default it picks up the right format from the environment:
| Where it's running | Default format | Why |
|-----------------------------------|----------------|--------------------------------------------------------------------------------------|
| Interactive terminal (TTY) | human | Coloured [level] [event] message lines so designers can read along. |
| Pipe / supervisor / container | json | One JSON object per line — { ts, level, event, msg, ... } — for log shippers. |
Override via the flag (--log-format=json) or the DESEN_LOG_FORMAT environment variable.
A minimal JSON log line looks like:
{"ts":"2026-04-25T12:30:00.123Z","level":"info","event":"sync_received","msg":"Sync wrote 4 file(s)","saved":4}Stable event slugs the daemon emits (use these for grep / alerting):
| Event | Level | When |
|-----------------------------|-------|-----------------------------------------------------|
| daemon_listening | info | HTTP + WebSocket server is up |
| daemon_shutdown | info | Graceful shutdown started |
| ws_client_connected | info | A WebSocket client connected |
| ws_client_disconnected | info | A WebSocket client closed |
| ws_error | error | WebSocket error event |
| sync_received | info | A sync payload was accepted and written |
| proposal_created | info | A proposal was queued for approval |
| proposal_approved | info | A proposal was approved |
| proposal_rejected | info | A proposal was rejected |
| canary_started | info | A canary rollout started |
| canary_auto_started | info | A canary started automatically after approval |
| canary_event | info | A canary state transition (other than rollback) |
| canary_rollback | warn | Canary was halted and rolled back |
| canary_restored | info | An in-flight canary was restored on daemon restart |
| audit_loaded | info | Audit log loaded from disk |
| audit_appended | info | New audit records were appended |
| pr_creating | info | A GitHub PR creation was kicked off |
| pr_created | info | A GitHub PR was successfully created |
| telemetry_processed | info | A telemetry batch was processed |
| telemetry_invalid | warn | A telemetry batch contained invalid envelopes |
| path_traversal_blocked | error | A /sync payload tried to write outside desen/ |
| validation_failed | error | A /sync payload failed Zod validation |
| sync_error / pr_request_error / telemetry_error | error | Top-level handler errors |
Privacy: log lines never contain payload bodies, telemetry envelopes, or audit chain hashes. Actor names, surface ids, proposal ids and risk levels are fine.
Daemon endpoints
Ops probes (always public)
| Endpoint | Status | Body |
|-----------------|---------------------|---------------------------------------------------------------------------------|
| GET /health | always 200 | { status: "ok", version, uptime_seconds } |
| GET /ready | 200 or 503 | { status, checks, not_ready? } — 503 with not_ready when any subsystem fails |
| GET /version | always 200 | { name, version, node_version } |
/ready reports on three subsystems:
workspace_mounted—desen-workspace/.desen/exists and is writable.governance_loaded— at least one successful read ofgovernance.json(or fall-through to defaults).audit_writer_healthy—audit.jsonlopened and the SHA-256 chain loaded without error.
These endpoints are deliberately public — any future auth middleware must be installed after them so container probes never get rejected.
Application endpoints
| Endpoint | Purpose | SPEC |
|---------------------------------------|--------------------------------------------|------|
| POST /sync | Accept a DSL batch from a Producer | §5 |
| GET /proposals | List pending proposals | §5.6 |
| POST /approval/approve/:id | Approve a proposal | §5.3 |
| POST /approval/reject/:id | Reject a proposal | §5.3 |
| GET /audit | Read the audit log + verify the chain | §5.4 |
| POST /rollout/halt/:surface_id | Halt an in-flight canary | §5.7 |
| POST /rollout/promote/:surface_id | Promote a canary to Stable | §5.7 |
| WebSocket | Broadcast telemetry + governance events | §4 |
Programmatic API
import { createDaemon, runValidate, runAudit } from "@desenlabs/cli";
const daemon = createDaemon({ port: 8888, workspace: "./desen" });
await daemon.listen();
// ... use runValidate / runAudit for headless pipelinesPhilosophy
This package is one implementation of the governance contracts in SPEC.md §5. It is not the protocol itself — another team can ship their own daemon in any language. The contract is what matters; this package is the reference.
Links
- Full command walkthrough: GETTING-STARTED.md
- Daemon architecture: IMPLEMENTATION-COOKBOOK.md
- Protocol: SPEC.md §5
- Release flow: RELEASING.md
