# CLI reference

The sufleur CLI — commands, configuration, and lockfile.

The Sufleur CLI is a Go binary that resolves your project's prompt dependencies from the registry, caches them locally, and generates typed code from them. The same binary ships through two language-specific wrappers; this page covers the binary itself. For installation, see the [Node](/docs/sdk/node) or [Python](/docs/sdk/python) SDK pages.

The CLI is open source: [github.com/sufleur/cli](https://github.com/sufleur/cli).

## Commands

Every command is run from a directory containing `sufleur.yaml`. Add `-v` / `--verbose` to any command to see detailed request logs.

### `sufleur init`

Interactive bootstrap. Prompts for your workspace name, the env var that holds your API key, the output language, and the output file path. Writes a starter `sufleur.yaml`. Refuses to overwrite an existing file.

### `sufleur add @workspace/prompt [constraint]`

Adds a prompt to `sufleur.yaml`, validates that it exists in the registry, then runs install. The constraint defaults to `*` (latest published).

```bash
sufleur add @acme/welcome-message            # latest published
sufleur add @acme/welcome-message ^1.2.0     # caret range
sufleur add @acme/welcome-message 0.1.0      # exact version
```

Flags:

| Name | Description |
| --- | --- |
| `--alias <name>` | Install the prompt under a different name in the same workspace, so you can keep multiple versions side-by-side. The alias becomes a separate entry in the generated SDK. |
| `--force` | Overwrite the entry if the prompt is already in `sufleur.yaml`. |

#### Installing a collection

A collection is a group of prompts, referenced as `@workspace/+name` — the `+` marks it as a collection rather than a prompt. Adding a collection expands it and adds **every** member prompt to `sufleur.yaml` under its own `@workspace/prompt` key (constraint `*`), then installs them:

```bash
sufleur add @acme/+onboarding-flows
```

Prompts already in `sufleur.yaml` are left untouched and reported as skipped; pass `--force` to reset them to `*`. From there each prompt behaves like any other entry — `update`, `generate`, and the lockfile all treat them individually (the collection itself is not tracked, so adding a prompt to the collection upstream won't appear until you re-run `sufleur add @acme/+onboarding-flows`).

### `sufleur remove @workspace/prompt`

Removes a prompt from `sufleur.yaml` and from the lockfile. Cleans up the on-disk cache only if no other alias still resolves to the same backing version. Aliased as `sufleur rm`.

### `sufleur install`

Reads `sufleur.yaml`, resolves every prompt against its constraint, fetches anything missing or stale into `.sufleur/`, and writes `sufleur-lock.yaml`. Run this after editing `sufleur.yaml` by hand, or on `git pull` to bring teammates in sync.

Flags:

| Name | Description |
| --- | --- |
| `--frozen` | CI mode. Fail if the lockfile is out of date relative to `sufleur.yaml`. Use this in your build pipeline to catch unintentional version drift. |

### `sufleur update [@workspace/prompt]`

Re-resolves prompts against the registry and updates the lockfile. With no argument, updates everything; with one argument, updates only that prompt.

### `sufleur generate`

Reads `sufleur-lock.yaml`, loads each prompt from the cache, and writes typed code to `output.file`. Emits one stderr line per Mustache inference warning so you can fix unannotated ambiguous variables.

If you change templates upstream, the order is: `update` → `generate`.

## Authoring commands

The commands above pull *published* prompts into your project. The commands below go the other way — they create and edit prompts and collections **in the registry**. This is also the surface a coding agent drives; run `sufleur skill` to print an agent-ready guide to it.

> [!NOTE] Different credentials
>
> Authoring commands authenticate with a personal **user** API key obtained via `sufleur login` — not the workspace key referenced in `sufleur.yaml`. They don't require a `sufleur.yaml` and can be run from anywhere. Every authoring command accepts `--json` for scripting.

### Authentication

```bash
sufleur login      # one-time device-code login; stores a user key in ~/.config/sufleur
sufleur me         # print the authenticated user (use to verify login)
sufleur logout     # revoke and forget the stored key
```

### Workspaces

List the workspaces you belong to, along with your role in each. Since the authoring commands are workspace-scoped, this is the quickest way to discover the `@workspace` names you can author in.

```bash
sufleur workspace list      # one row per workspace: @name, your role, display name, description
```

### Prompts

```bash
sufleur prompt create @acme/welcome --description "Greets a new user"
sufleur prompt get    @acme/welcome
sufleur prompt list   @acme --search welcome --limit 50
sufleur prompt update @acme/welcome --description "Updated copy"
```

`create` and `update` only set the description; **visibility is intentionally not editable from the CLI** (see below).

### The draft workflow

A published version is immutable — the registry rejects writes to it. You edit a **draft**, then publish from the web app. The loop is: create a draft, edit its files/metadata/README, then publish.

```bash
sufleur version draft @acme/welcome            # fork the latest published version into a draft
sufleur version list  @acme/welcome [--status DRAFT|PUBLISHED]
sufleur version get   @acme/welcome@draft
sufleur version dump  @acme/welcome@draft --to ./working   # export files, schema, README, metadata to disk
sufleur version delete @acme/welcome@draft
```

README, metadata, and output schema live on the version:

```bash
sufleur version get-readme @acme/welcome@1.2.0
sufleur version set-readme @acme/welcome@draft --file ./README.md   # or --content STR, or --file - for stdin
sufleur version set-output-schema @acme/welcome@draft --file ./schema.json
sufleur version delete-metadata @acme/welcome@draft --key old-key
```

`set-metadata` has two mutually-exclusive modes:

| Name | Description |
| --- | --- |
| `--from-file <path>` | Full sync from a flat YAML file — keys absent from the file are deleted. |
| `--string / --int / --float / --bool KEY=VALUE` | Additive, typed single-key patches that leave other keys untouched. Repeatable. |

### Files

Files live inside a draft version. Names are stored without the `.mustache` suffix; the CLI strips it on write and re-adds it on `dump`.

```bash
sufleur file list   @acme/welcome@draft
sufleur file create @acme/welcome@draft --file ./welcome.mustache [--name override] [--entrypoint]
sufleur file update @acme/welcome@draft --name welcome --file ./welcome.mustache   # or --content STR
sufleur file update @acme/welcome@draft --name welcome --rename greeting
sufleur file delete @acme/welcome@draft --name old-file
sufleur file set-entrypoint @acme/welcome@draft --name welcome [--clear]
```

To verify a draft without round-tripping, render it locally with `sufleur prompt render ./working --entrypoint welcome --vars '{...}'` (see the [Mustache extensions](/docs/mustache) page).

### Collections

A collection is a workspace-scoped group of prompts, referenced as `@workspace/+name` (the `+` marks it as a collection). Unlike prompts, collections have **no draft workflow** — every edit applies immediately.

```bash
sufleur collection create       @acme/+onboarding --description "New-user flows"
sufleur collection get          @acme/+onboarding          # metadata + README
sufleur collection list-prompts @acme/+onboarding          # one @acme/name per line
sufleur collection link         @acme/+onboarding @acme/welcome   # add a prompt to the collection
sufleur collection set-readme   @acme/+onboarding --file ./README.md
sufleur collection set-description @acme/+onboarding --content "..."
```

A prompt belongs to at most one collection, so `link`ing a prompt that already sits in another collection moves it — that's refused unless you pass `--force`. To pull a whole collection into a project, use [`sufleur add @workspace/+name`](#installing-a-collection).

### Evals

An **eval** scores a prompt version against a [dataset](/docs/datasets): it pins the dataset, the candidate's input mapping, optional LLM judges, CEL assertions, and a passing threshold. (Provider, model, and parameters come from each prompt version's model config — set with `sufleur version set-model-config` or in the web app.) Evals are authored as YAML on a draft version — the loop is get → edit → validate → push:

```bash
sufleur eval get @acme/extraction@draft --file ./eval.yaml       # current YAML (skeleton if none)
sufleur eval validate @acme/extraction@draft --file ./eval.yaml  # parse + type-check, save nothing
sufleur eval push @acme/extraction@draft --file ./eval.yaml      # validate, then save
sufleur eval delete @acme/extraction@draft                       # remove the eval
```

Then run and inspect. `eval run --watch` and `eval watch <id>` exit non-zero on a failing verdict, so either doubles as a CI gate:

```bash
sufleur eval run @acme/extraction@draft [--watch]   # enqueue (and optionally follow) a run
sufleur eval runs @acme/extraction@draft            # list recent runs, newest first
sufleur eval show <run-id>                           # one run's status, verdict, score
sufleur eval watch <run-id>                          # follow an in-flight run
```

Datasets are created in the web app and referenced via `dataset.ref`. See the [Evals](/docs/evals) page for the full YAML schema and CEL reference, and [Datasets](/docs/datasets) for how cases and schemas work.

> [!WARNING] What the CLI won't do
>
> Destructive and high-risk actions are intentionally absent and stay human, web-app actions: **publishing** a draft, changing **visibility** (public ↔ private), **deleting** a published version, **deleting** a collection, and **unlinking / removing** a prompt from a collection. There is no `publish`, `visibility`, or `unlink` command — don't look for one.

## `sufleur.yaml`

The project file. Three top-level keys, all required:

```yaml
api_keys:
  acme: ${ACME_API_KEY}
  internal-tools: ${INTERNAL_API_KEY}

prompts:
  '@acme/welcome-message': '*'
  '@acme/legacy-welcome': '@acme/welcome-message@0.1.0'

output:
  language: typescript
  file: ./generated/prompts.ts
```

| Name | Type | Description |
| --- | --- | --- |
| `api_keys` | `map` | One entry per workspace you fetch prompts from. The value uses the `${VAR}` syntax to reference an env var resolved at runtime — never put the literal key in version control. |
| `prompts` | `map` | Map of `@workspace/name` → constraint. The plain form ties the alias key to the same package; the `@workspace/pkg@constraint` form lets the same backing prompt be installed under a different alias (useful for keeping an old version alongside the latest). |
| `output.language` | `string` | Either `typescript` or `python`. |
| `output.file` | `string` | Where the generated code is written. Typically `./generated/prompts.ts` or `./generated/prompts.py`. |

### Environment variables

API keys are referenced by env var, not embedded. The CLI loads a local `.env` file automatically (via [godotenv](https://github.com/joho/godotenv)) before resolving references. If a referenced variable is unset, the command fails with a clear message naming the missing variable.

```bash
# .env
ACME_API_KEY=sk-...
INTERNAL_API_KEY=sk-...
```

## Workspaces, naming, and semver

Prompt names follow `@workspace/name`, the same shape as scoped npm packages. The workspace prefix is the permission boundary — your API key is scoped to a single workspace.

Constraints accept the full [semver](https://semver.org) range syntax: `*` (any), `1.2.3` (exact), `^1.2.0` (compatible-major), `~0.1.0` (compatible-minor), `>=1.0.0 <2.0.0` (range). The string `draft` resolves to the current draft version of the prompt, which is mutable — `install` will warn loudly if you depend on a draft.

## The lockfile

`sufleur-lock.yaml` is generated by `install` / `update` and **must be committed**. Every entry pins:

- The exact resolved version
- The integrity SHA-256 of the cached payload
- The constraint that produced the resolution
- The version's status (`PUBLISHED` or `DRAFT`)

```yaml
resolved:
  '@acme/welcome-message':
    version: 1.2.3
    integrity_sha: sha256-...
    constraint: '*'
    status: PUBLISHED
    resolved_at: 2026-04-12T18:23:11Z
```

`generate` reads the lockfile, not `sufleur.yaml`, which guarantees that two developers with the same lockfile produce byte-identical code. `--frozen` in CI catches the case where someone forgot to commit a refreshed lockfile.

> [!WARNING] Don't edit the lockfile by hand
>
> The integrity hashes are checked at generate time. Edit `sufleur.yaml` and run `install` —
> never modify `sufleur-lock.yaml` directly.

## Cache

Resolved prompts are cached under `.sufleur/` in your project. The cache key includes the package ref + version, so multiple aliases of the same underlying version share a single cache entry. Add `.sufleur/` to your `.gitignore` — the lockfile is the source of truth, and `install` will rebuild the cache from it.
