---
slug: 70-cli
title: CLI
description: La línea de comandos de Synsema — run, test, check, serve, conform, repl, daemon — más los flags y exit codes que la hacen apta para scripts y CI.
example_ids: []
---

# CLI

Un solo binario estático. Los comandos centrales:

```sh
synsema init [dir]             # scaffold: hello.syn (tour del lenguaje + test), .env.example, .gitignore, .mcp.json (synsema-code)
synsema init [dir] --synfide   # + instala el framework Synfide (workflows durables, bandeja de
                               #   aprobaciones, kv persistente), fijado a su ultimo release:
                               #   sha256 por archivo, synfide/VERSION registra el tag, re-ejecutar
                               #   actualiza (solo archivos del framework — los tuyos jamas se
                               #   pisan, y un archivo del framework que VOS editaste tampoco: la
                               #   version nueva queda al lado como <archivo>.new con aviso fuerte),
                               #   mas un app.syn de arranque y su suite de tests
synsema init [dir] --pwa       # una app INSTALABLE (v0.6.15+): app.syn + index.html + public/{manifest.webmanifest,
                               #   sw.js, app.js, icon.svg} + los íconos PNG generados desde el SVG + push_keys.syn
                               #   (par VAPID para push nativo) — ver "Tu app en el teléfono"
synsema run program.syn        # ejecutar (sale cuando el programa termina); `-` lee el fuente desde stdin
synsema run program.syn -- a b # todo lo que va después de `--` es el argv propio del programa → args() == ["a", "b"]
synsema test program.syn       # correr bloques `test` (un archivo o un directorio)
synsema build program.syn -o app  # un único binario autocontenido: el engine + tu programa horneados adentro
synsema check program.syn      # sin ejecutar: parse + resuelve todos los `use` + valida los templates de render("…") + (v0.6.19+) rechaza lo que serve rechazaría en un grupo `export routes` (stream/socket)
synsema code outline [path]    # inteligencia de código para agentes (v0.6.13+): outline / symbol / refs / routes / caps /
                               #   check / search / deps — estático, del parser (--json); `synsema code --mcp` sirve las
                               #   mismas tools como el servidor MCP `synsema-code` que `init` registra en .mcp.json
synsema openapi app.syn        # el /openapi.json que publicaría el server, desde el fuente — sin ejecutar,
                               #   sin puerto (--out openapi.json, --base-url https://api.example); exit 2 sin `serve`
synsema serve program.syn      # queda vivo para HTTP / crons / agentes (--watch = reinicia al cambiar un .syn)
synsema repl                   # REPL interactivo
synsema conform --swarm app.syn  # dump de estado post-corrida (blackboard + agentes) en JSON
synsema daemon start app.syn   # daemon en background (ver Deploy)
synsema llm status             # config LLM resuelta + diagnóstico (--json para scripting)
```

### Re-correr `init` es seguro — y repara

`init` nunca decide por la mera existencia. Cada archivo que administra se clasifica por
**procedencia**:

| Su contenido | Qué pasa |
|---|---|
| idéntico a la versión actual | `ya está al día` — no se toca |
| idéntico a **alguna** versión anterior publicada | `actualizado (estaba sin ediciones tuyas)` — seguía de fábrica, recibe la nueva |
| no coincide con ninguna versión publicada | es tuyo: se conserva, y la versión nueva queda al lado como `<archivo>.new` |

Nadie escribe a mano un archivo byte-idéntico a un release viejo, así que coincidir es prueba
suficiente de que no hay trabajo tuyo que proteger. Eso es lo que permite que un proyecto que
venía salteándose upgrades se ponga al día en vez de quedar congelado — y con `--synfide`,
re-correr también **repara** un scaffold con archivos borrados o desfasados, incluso cuando
`synfide/VERSION` ya nombra el último release (engine v0.5.9+; antes, existir alcanzaba para
considerarlo tuyo).

### `init --pwa` — una app instalable (engine v0.6.15+)

`synsema init [dir] --pwa` genera un sitio que se instala en Android, iOS y escritorio: `app.syn`
(el server: `static "/" from "./public"`, la página y `mount api.api`), `api.syn` (v0.6.19+: el API
como grupo `export routes` — `/api/ping` y las rutas de push), `index.html`,
`public/manifest.webmanifest`, `public/sw.js` (service worker), `public/app.js`, `public/icon.svg`
y `push_keys.syn` (imprime el par VAPID para `.env`). Los íconos PNG (`icon-192.png`,
`icon-512.png`, `apple-touch-icon.png`) se **generan desde `icon.svg`** con el rasterizador del
propio engine — editá el SVG y volvé a correr `init --pwa` para refrescarlos; los PNG que trajiste
vos quedan intactos mientras `icon.svg` sea el de fábrica, y no se tocan nunca si borraste el SVG.
Sin `hello.syn` (el starter es `app.syn`); los archivos base (`.env.example`, `.gitignore`,
`.mcp.json`) vienen como siempre, con las mismas reglas de procedencia — re-correr jamás pisa lo
tuyo. `--pwa` y `--synfide` son starters distintos: pasar los dos es error de uso (exit 2), igual
que un flag desconocido. El directorio puede ir antes o después del flag. Recorrido: **[Tu app en
el teléfono (PWA)](/es/0.6.x/41b-pwa)**. Desde engine v0.6.19 el API vive en `api.syn` (un grupo
`export routes api` que `app.syn` monta) para que las mismas rutas sirvan a una entrada de
escritorio; un `app.syn` de fábrica de un `init --pwa` anterior se refresca al modular, uno editado
se conserva (`app.syn.new` al lado).

### `init --desktop` — la misma app como app de escritorio (engine v0.6.19+)

`synsema init [dir] --desktop` escribe todo lo de `--pwa` más `desk.syn` — la entrada de
escritorio: `bind "127.0.0.1"`, `mount api.api`, la página renderizada con `desktop: true`, una
ruta `socket` que cuenta las ventanas abiertas, el navegador abierto como ventana de app con
`run()` bajo `exec` (`platform()` elige `cmd` / `open` / `xdg-open`), y `shutdown()` cuando se
cierra la última ventana o cuando ninguna se abrió en 30 s — y `public/desk.js`, el socket que
abre cada ventana (`index.html` lo carga sólo bajo `{ when desktop }`). `DESK_NO_WINDOW=1` saltea
el navegador (tests, CI). `synsema serve desk.syn` abre la ventana; `synsema build desk.syn -o desk
--serve --no-console --icon public/icon.svg [--bundle]` la distribuye. `--desktop` sobre un
proyecto `--pwa` existente sólo agrega los dos archivos. Recorrido: **[Tu app en el
escritorio](/es/0.6.x/41c-desktop)**.

### Qué cubre el `.env.example`

El `.env.example` generado viene comentado sección por sección: los pares de provider LLM
(provider + SU key), los techos del host que el programa no puede subir
(`SYNSEMA_SPEND_CEILING` y el techo por identidad `SYNSEMA_SPEND_CEILING_PER_IDENTITY`), los
knobs de aprobación humana y, al final, **tus propios secretos** — los que nombra tu código:
`JWT_KEY` para `jwt_sign`, `CAPTOKEN_ROOT_KEY` para `captoken_mint` (atenuar no necesita
clave, por eso un subagente delegado nunca la ve) y `AGENT_SIGNING_KEY` para `http_sign` (que
además necesita `require sign("AGENT_SIGNING_KEY")`). El nombre que elegís *es* el scope de la
capability, así que agregá los tuyos con el mismo patrón. Ver [Secretos](/es/0.6.x/21-secrets)
e [Identidad de agentes](/es/0.6.x/46-agent-identity).

## `synsema code` — inteligencia de código para agentes

Un agente que trabaja sobre un repo Synsema no debería leer archivos enteros para saber qué tienen. `synsema code` responde desde el parser — nunca ejecuta el programa ni habla con un server corriendo:

```sh
synsema code outline                # cada .syn: intent, requires, imports, tasks/agentes/tipos/rutas/tests con rango de líneas
synsema code routes app.syn         # la tabla HTTP que un `serve` publicaría (auth, stream/socket/proxy, expect, capabilities)
synsema code refs send_report       # dónde se usa una task/agente/tipo, con el símbolo que lo contiene
synsema code caps                   # capabilities declaradas vs necesarias, y qué FALTA (con el `require` a agregar)
synsema code check                  # parse + imports + templates de todo el proyecto (exit 1 con errores; warnings = caps faltantes)
synsema code search "proxy to"      # búsqueda de texto en .syn/.html/.js/.css/… con el símbolo que la contiene; --regex, --kinds, --limit
synsema code deps                   # grafo task → task + imports
```

Con `--json` sale la estructura exacta. Las mismas ocho tools las sirve por MCP `synsema code --mcp`, registrado como **`synsema-code`** en el `.mcp.json` que escribe `synsema init` — así cualquier agente que abra la carpeta las descubre solo. Es tooling de desarrollo sobre el código de esa carpeta, **no** el MCP de tu aplicación (eso es discovery: `/openapi.json`, `/llms.txt`). Referencia completa: la página `code.md` del skill para agentes.

## `synsema build` — un programa, un binario

`synsema build main.syn -o app` produce un **único ejecutable autocontenido**: copia el engine
en ejecución y le agrega tu programa — el `.syn` principal, los módulos `use` transitivos que
importa, los templates que `render`iza por un nombre literal, y todo lo que sumes con `--include` —
sellado con un trailer y un sha256. El resultado es un archivo, como `docker` es un archivo; sin
Python, sin npm, nada que instalar en el destino.

```sh
synsema build lamp.syn -o lamp                       # el binario para esta plataforma
synsema build lamp.syn -o lamp --include assets/     # empaqueta un directorio (recursivo) …
synsema build lamp.syn -o lamp --include "data/*.csv"  # … o un glob de un nivel, repetible
synsema build lamp.syn -o lamp --sandbox             # hornea un techo del host en el binario
synsema build lamp.syn -o lamp --cap-set "stdout,net=api.example.com"   # techo horneado a medida
synsema build lamp.syn -o lamp --profile pure        # hornea el perfil pure (sin filesystem/exec/db…)
synsema build lamp.syn -o lamp-linux --engine-binary ./synsema-linux-x86_64  # "cross": un engine donante
synsema build app.syn -o app --serve --bind 127.0.0.1                  # un binario SERVER (v0.6.16+): corre el
synsema build app.syn -o app --serve --bind 0.0.0.0 --port 8080 --secure   #   runtime de serve con estos flags de
synsema build app.syn -o app --serve --bind 0.0.0.0 --domain app.example.com --tls-auto ops@example.com   # deploy horneados
synsema build desk.syn -o desk --serve --no-console --icon icon.svg     # una app de ESCRITORIO (v0.6.18+): desk.exe con tu ícono y
synsema build desk.syn -o desk --serve --icon icon.svg --bundle          #   sin ventana de consola; .app / dir + install.sh en macOS / Linux
```

Imprime `built <out> (<n> files, <bytes>)`. El bundle es **cerrado**: un `use` cuyo path es una
expresión de runtime no se puede resolver estáticamente y es un error de build, no un hueco
silencioso; un `--include` que se escapa del directorio del programa se rechaza; los assets más
grandes que el engine no son problema (el binario pesa ~30–60 MB de cualquier forma — nadie pesa la
CLI de Docker).

**Un server en un binario (engine v0.6.16+).** Un programa con un bloque `serve on` sólo corre bajo
el runtime de serve, así que se construye con `--serve` (sin el flag el build para con exit 2 y lo
dice, en vez de que el binario falle al correr). El bind es **obligatorio** — un distribuible tiene
que decir dónde escucha (`127.0.0.1` para una app local, `0.0.0.0` para una pública): `--bind`, o la
cláusula `bind "…"` del bloque serve (v0.6.18+; el flag le gana); `--port`,
`--domain`, `--tls-auto`, `--tls-cert`/`--tls-key` y `--secure` son los mismos knobs de deploy que
`synsema serve`, horneados (los archivos TLS se leen del disco al arrancar — la clave nunca viaja
dentro del binario). Los **mounts estáticos del bloque serve se empaquetan solos** (`static "/x"
from "./public"` → entra `public/`; un directorio que falta es error de build) y se sirven **desde
el bundle antes que del disco**, con ETag por contenido — la [app instalable](/es/0.6.x/41b-pwa)
entera cabe en el único archivo. Ctrl-C / SIGTERM es el shutdown ordenado, exit 0. `--serve` con
`--profile pure` se rechaza (un server bindea un socket).

**Una app de escritorio en un binario (engine v0.6.18+).** La cláusula `bind "127.0.0.1"` del bloque
serve se hornea cuando no hay `--bind` (sin ninguno de los dos, el build para con exit 2). `-o desk`
pasa a ser `desk.exe` cuando el motor que se envuelve es un ejecutable de Windows; `--no-console` no
abre ventana de consola (stdout/stderr se descartan); `--icon <svg|png|ico>` pone tu ícono en el
`.exe`, el `.app` o el lanzador de Linux; `--bundle [--name "Mi App"] [--id com.example.app]` escribe
`Mi App.app/` (macOS) o `<stem>/` + `install.sh` (Linux) — en Windows no hay nada que hacer, y la línea
`built` lo dice. Todo lo decide el **formato del motor** (PE / Mach-O / ELF), nunca la máquina que
construye. La receta — el navegador como ventana de app, un socket como verdad de "hay una ventana
abierta", `shutdown()` cuando se cierra la última — está en **[Tu app en el escritorio](/es/0.6.x/41c-desktop)**.

**El binario construido corre tu programa.** Todo su `argv` es el del programa (`args()`), el techo
y el perfil horneados aplican, y lee su propio bundle — un `read_file("assets/x")` sobre un archivo
empaquetado **no necesita la capability `file`** (es parte del programa, como un `use`), y una
escritura a disco sobre un path empaquetado se rechaza. `render`izar un template empaquetado tampoco
necesita capability.

**Llegar al engine:** `app --engine <subcommand>` corre la CLI pelada del engine dentro del binario
construido — la única entrada, ya que `--sandbox` y compañía ahora son el argv del programa. `app
--engine version` imprime la versión del engine; `app --engine run other.syn` corre otro programa.
(El binario `synsema` de fábrica también acepta `--engine` como prefijo no-op, así los scripts
quedan uniformes.) `app --engine update` se rechaza — un programa construido se reconstruye con
`synsema build`, no se actualiza en el lugar. Un bundle manipulado (`sha256 mismatch`) se niega a
correr.

Deployalo directo desde `FROM scratch`/distroless — ver **[Deploy](/es/0.6.x/71-deploy)**.

## `synsema llm status`

Imprime la configuración LLM que el runtime va a usar **de verdad** — cada valor con su fuente
(`environ` / `.env` / `default`), la key solo como **presencia** (jamás valores, prefijos ni
longitudes), qué archivo `.env` se cargó, y un aviso si hay varios binarios `synsema`
pisándose en el `PATH`. Cuando está offline nombra la variable exacta que falta — incluida la
pista *"hay una clave bajo `DEEPSEEK_API_KEY`: ¿la guardaste bajo la variable equivocada?"*.
No toca la red. Exit `0` = vivo, `1` = offline (scripteable: `synsema llm status && synsema
serve app.syn`). Referencia completa de knobs: **[Provider config](/es/0.6.x/52-llm-provider-native)**.

## Flags útiles

| Flag | Comando | Efecto |
|---|---|---|
| `--flat` | `run` / `test` / `conform` | parsear un archivo `.fsyn` (documento plano) (también se autodetecta por la extensión `.fsyn`) |
| `--explain` | `run` | reporte rico de error en stderr (contexto, call stack, sugerencias) |
| `--format json` | `run` | **sin** `--explain`: toda la corrida como un único documento JSON en stdout — `{ok, output, errors, audit, exit, llm_tokens}` (la forma nativa del `run()` de wasm; la salida se junta, no se streamea). **Con** `--explain`: diagnósticos de error estructurados |
| `--provider <name>` | `run` | forzar el proveedor LLM (`anthropic`/`openai`/`minimax`/`deepseek`) |
| `--sandbox` | `run` / `test` / `conform` / `serve` | techo del host `stdout,time` — ejecutar código no confiado (ver **[Capacidades](/es/0.6.x/20-capabilities)**). Bajo `serve` (engine v0.6.7+) suma `serve` para poder bindear, y acota por igual el **preámbulo**, los handlers, los ticks de cron y los agentes spawneados |
| `--cap-set "<lista>"` | `run` / `test` / `conform` / `serve` | techo del host a medida (`name` o `name=scope`, p. ej. `"stdout,time,serve=8080,net=api.example.com"`); `--cap-set none` = un techo vacío (nada, ni siquiera `stdout`); mutuamente excluyente con `--sandbox` |
| `--profile native\|pure` | `run` / `test` / `conform` / `build` | `pure` (default `native`) es un **segundo muro**: cada builtin de filesystem/exec/socket/db/cron falla con la verdad del entorno, independiente del techo — ver **[Sandbox § el perfil pure](/es/0.6.x/22-sandbox)**. `serve --profile pure` es un error de uso (un server bindea un socket) |
| `--audit json\|<path>\|fd:N` | `run` / `test` / `conform` / `serve` | una línea JSON por chequeo de capability — `{ts, context, capability, granted, source, reason, origin, file, line}` — a stderr (`json`), un archivo, o un fd (solo Unix), más una línea final `{"summary": {granted, denied, exit}}`. Los **valores** de secretos nunca aparecen (solo nombres). Ver **[Observabilidad](/es/0.6.x/63-observability)** |
| `--env-file <path>` / `--no-env-file` | todos | override / desactivar la carga del `.env` |
| `--out <archivo>` / `--base-url <url>` | `openapi` | escribir el documento a un archivo en vez de stdout / fijar el `servers` de OpenAPI (default: ninguno — el server en vivo lo deriva de `domain`/`Host`) |
| `--watch` | `serve` | loop de dev: reinicia ante cualquier cambio de `.syn` (templates/estáticos ya se recargan por request) |
| `--port` / `--domain` / `--tls-auto` / `--bind` / `--secure` | `serve` | knobs de deploy (ver **[Deploy](/es/0.6.x/71-deploy)**) |
| `--serve --bind <addr>` [`--port` `--domain` `--tls-auto` `--tls-cert` `--tls-key` `--secure`] | `build` | (v0.6.16+) un binario server: el runtime de serve con estos knobs horneados; el bind es obligatorio (`--bind`, o la cláusula `bind "…"` del bloque, v0.6.18+); los mounts estáticos del serve se empaquetan solos |
| `--no-console` / `--icon <svg\|png\|ico>` / `--bundle [--name <n>] [--id <id>]` | `build` | (v0.6.18+) escritorio: subsistema GUI (Windows), el ícono de la app (recurso del `.exe` / `.icns` / PNG del lanzador), un `Nombre.app` (macOS) o un `dir/` + `install.sh` (Linux) — todo según el formato del motor; el `.exe` se agrega solo para un motor Windows — ver **[Tu app en el escritorio](/es/0.6.x/41c-desktop)** |
| `-o <archivo>` / `--include <p>` / `--engine-binary <p>` | `build` | path de salida / empaquetar un archivo, directorio (recursivo) o glob `*`/`?` (repetible) / construir contra un binario de engine donante (cross) — ver **[`synsema build`](#synsema-build--un-programa-un-binario)** |
| `-- <args...>` | `run` | fin de los flags del engine: todo lo que va después de `--` es el argv propio del programa, legible con `args()` |
| `--engine <subcommand>` | cualquier binario | correr la CLI pelada del engine (un prefijo no-op en `synsema`; la única entrada al engine de un binario `synsema build`) |

`conform` respeta `--sandbox`/`--cap-set`/`--profile`/`--audit` con el mismo significado que `run`
(engine v0.6.14+; antes los ignoraba en silencio — el único subcomando que no hacía sandbox). Su
stdout sigue siendo el contrato JSON `{ok, out, err}`; una denegación aparece dentro de `err`.

## REPL

`synsema repl` abre una sesión interactiva: cada línea es una sentencia top-level, y **el estado persiste entre líneas** (un `let` de una línea se ve en la siguiente). Los resultados se muestran con `print`/`show` — las expresiones peladas se evalúan pero no se ecoan:

```sh
$ synsema repl
>>> let who be "repl"
>>> show "hola " + who
hola repl
>>> print(type_of(who))
text
```

Salís con **Ctrl+D** (Ctrl+Z y Enter en Windows). También funciona no-interactivo — pipeá sentencias (`printf '...' | synsema repl`) para scriptear chequeos rápidos.

## Exit codes

`0` al tener éxito; `1` ante un error de parseo, un error de runtime, o si algún **agente** spawneado terminó en `ERROR`; `2` ante un error de uso — un argumento faltante, `synsema openapi` sobre un archivo sin `serve`, o (engine v0.6.14+) **un `--flag` desconocido**, que ahora se rechaza en vez de ignorarse en silencio en `run`/`test`/`conform`. El `run` plano imprime la línea estable `Runtime error: file:line:col: msg`; agregá `--explain` para el reporte rico. (Para medir exit codes en una shell, no pipees antes de `echo $?` — redirigí.) Desde engine v0.6.19 una salida que nadie lee no es una falla: cuando el lector de stdout o stderr se fue (`synsema run x.syn | head -1`, un padre que cerró la tubería, PowerShell capturando un programa GUI al que no espera) el proceso termina **en silencio con 0** — la convención de SIGPIPE — en vez de un pánico de Rust.

> **v0.6.14, breaking a propósito.** Tres cosas por las que un programa que antes corría ahora puede
> frenar, cada una para mantener el techo honesto: un flag desconocido es un error (arriba); bajo un
> techo del host, `stdout` es una capability real, así que `--cap-set` sin `stdout` niega la salida
> en el primer `print` (`--sandbox` lo sigue incluyendo); y un `render` de un template en **disco**
> necesita `require file.read("…")` como cualquier otra lectura de archivo (los templates
> empaquetados y los `include`/`layout` anidados no).
