@zitadel/cli 1.0.0-alpha.23 → 1.0.0-alpha.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILLS.md DELETED
@@ -1,529 +0,0 @@
1
- ---
2
- name: zitadel-cli
3
- description: >-
4
- Set up and manage Zitadel authentication in a local project with the
5
- agent-friendly `zitadel` CLI. Use when the user wants to add login,
6
- registration, or session handling, create a Zitadel project, scaffold auth
7
- for a Next.js, React, Vue, Angular, Nuxt, Solid, Svelte, or Qwik app, or plan and apply Zitadel
8
- config changes from repo state.
9
- ---
10
-
11
- # Zitadel CLI
12
-
13
- The `zitadel` CLI integrates Zitadel auth into an existing project and keeps the
14
- local config (`zitadel.json` and `.zitadel/**`) as the source of truth. Every
15
- command emits a JSON envelope, so you should drive it non-interactively and
16
- parse the result rather than scraping human output.
17
-
18
- ## Invocation rules
19
-
20
- - Always pass `--non-interactive --json`. The envelope is the contract.
21
- - Add `--cwd <path>` when operating outside the current working directory.
22
- - Never run interactive prompts; `--non-interactive` (and `--json`) disable them.
23
- - The CLI sends anonymous usage telemetry by default. For automated/agent runs
24
- that should stay silent, disable it with `--no-telemetry` (per invocation) or
25
- `ZITADEL_TELEMETRY=0` / `DO_NOT_TRACK=1` (per environment); this also skips the
26
- small end-of-command network flush and drops the `ci/` and `host/` tokens from
27
- the CLI's HTTP `User-Agent`.
28
- - See `README.md` (its commands section is generated from the CLI's own
29
- metadata) or run `zitadel <command> --help` for the full per-command flag list.
30
-
31
- ```sh
32
- npx @zitadel/cli@alpha <command> --non-interactive --json
33
- ```
34
-
35
- During the public alpha, bare `npx @zitadel/cli` is promoted to the same tested
36
- alpha CLI so discovery works for first-time users. Prefer `@alpha` or an exact
37
- `0.1.0-alpha.N` selector in agent scripts and bug reports for reproducibility.
38
-
39
- ## Reading the envelope
40
-
41
- Each invocation prints one JSON object:
42
-
43
- - `status`: `ok` | `skipped` | `error`.
44
- - `cli_version`, `command`, `source`: always present.
45
- - On success: `data` with the command-specific payload.
46
- - On a no-op: `reason` (e.g. `no-framework-detected`, `orphaned-config`).
47
- - On failure: `code` (e.g. `E_VALIDATION`, `E_NETWORK`, `E_NOT_FOUND`,
48
- `E_CONFLICT`) and `message`.
49
- - `next_commands`: the suggested follow-ups. Prefer these over free-text hints.
50
- - `plan` and `apply` also emit `data.changes`: one row per touched resource
51
- (`{kind, action, file, id?, previous_id?}`, action ∈ create/update/revision/
52
- delete). Plan rows preview; apply rows report, with the resulting platform
53
- ids. Use it to verify an edit did what you intended — `apply`'s
54
- `files_updated` lists only local write-backs, not platform changes.
55
- - `setup` emits `data.files`: one typed row per scaffolded artifact
56
- (`{path, kind: file|dir, action: create|update}`), deduplicated. Use it to
57
- see what setup created versus merged into (your `package.json` is an
58
- `update`). `data.files_written` remains the flat list — deduplicated file
59
- paths only, covering both scaffolded and `.zitadel/` resource files.
60
- - `setup` also emits `data.design`: the starter login design it ejected and
61
- published as branding revision 1, or `null` when the built-in template was
62
- kept (no `.zitadel/branding/` files exist in that case). Use it to verify
63
- the requested `--design` took effect without diffing the repo.
64
- - `E_LOCAL_SERVER_NOT_RUNNING`: start the local runtime with
65
- `npx @zitadel/cli@alpha start`, then retry with `--server local`.
66
- - `E_NOT_FOUND`: an HTTP 404 from the target server. With the platform's
67
- error envelope it names a missing resource (e.g. an unknown schema id);
68
- without one the endpoint itself is missing — the `--server` value likely
69
- points at something that is not a Zitadel platform API. Follow
70
- `next_commands` (usually `start` + retry with `--server local`).
71
- - `E_PORT_IN_USE`: the requested local runtime port already has a listener.
72
- Stop that process, run `npx @zitadel/cli@alpha stop --all` for host-wide
73
- CLI-managed local runtimes, or choose another `start --port`.
74
-
75
- In human mode the output follows the terminal: a TTY gets the project and
76
- server lines and an aligned table, while a piped or redirected run (or
77
- `--plain`) gets one tab-separated record per line and nothing else. `--json`
78
- is unaffected and remains the contract for agents.
79
-
80
- Text the server returns is escaped before it is printed, in `--json` as in
81
- human mode, so a value someone stored cannot drive the reader's terminal.
82
- Control, format and bidi characters in `data` values and keys and in an
83
- error's `message` appear as visible `\xNN`, `\uNNNN` or `\u{NNNNN}` text;
84
- newlines and tabs are kept, and a key's backslashes are doubled so two keys
85
- never merge. A value that contained such a character is therefore not the
86
- stored value byte for byte: do not send it back in an update as if it were.
87
- When you need the exact stored value, call the platform API directly with the
88
- project secret; the CLI only ever shows the escaped form.
89
- `plan`, `apply` and `setup` are the exception for `.zitadel/` files: they
90
- write the server's bodies back verbatim and escape only what they print.
91
-
92
- A property whose name reads as a credential (`password`, `client_secret`,
93
- `api_key`, …) is refused anywhere on the command line — as an `--attributes`
94
- entry and inside an inline `--data` body alike, since argv is visible to other
95
- processes and kept in shell history. Send such bodies with `--file <path>` or
96
- `--file -` (stdin), which never pass through argv.
97
-
98
- Capture stdout and stderr separately when scripting. Some terminals and agent
99
- UIs display both streams together, but the machine contract is one parseable
100
- JSON object on stdout; installer, audit, and package-manager progress belongs
101
- on stderr.
102
-
103
- Exit codes mirror the error class (3 = validation, 4 = network or not-found,
104
- 5 = conflict, 1 = auth, 2 = not-implemented). An unknown command is handled by
105
- the CLI's help layer, not the envelope.
106
-
107
- ## Resource commands
108
-
109
- Runtime resources — users, teams, sessions, events, grants, projects — have a
110
- uniform `zitadel <resource> <verb>` surface built from one registry
111
- (`src/commands/resources.ts`); the conventions are documented in
112
- `docs/design/cli/resource-commands.md`. Config resources (schemas, flows,
113
- branding) stay on `plan` / `apply`; the resource commands never write them.
114
-
115
- | Resource | Verbs |
116
- | ---------- | -------------------------------------- |
117
- | `users` | list, get, create, update, delete |
118
- | `teams` | list, get, create, update, deactivate |
119
- | `sessions` | list, get, revoke |
120
- | `events` | list, get |
121
- | `grants` | list, get, create, delete |
122
- | `projects` | list, get, update |
123
-
124
- Every verb reads the project credential from `.zitadel/secret` and injects
125
- `project_id` itself; there is no project flag.
126
-
127
- `zitadel resources --json` reports the whole surface in one call: every
128
- resource, its verbs, its `filter_fields` / `sort_fields`, and the fields of its writes,
129
- reported per verb as `create_fields` and `update_fields` (flag name, kind,
130
- `required`, and any closed value set), and `delete_outcome`, the property a
131
- delete's envelope carries beside `id`. Prefer it
132
- over reading `--help` per command. It contacts no server.
133
-
134
- - `list` emits `data: { items, count, next_page_token }`, and when a page
135
- remains, `data.next_commands` carries the exact command for the next page —
136
- the cursor repeated alongside the same `--limit`, `--sort` and `--filter`,
137
- since a token is only valid with the query that issued it. Prefer it over
138
- rebuilding the invocation yourself. One page by default
139
- (`--limit`, `--page-token`); `--all` drains every page and sets
140
- `next_page_token` to `null`. Query-backed lists take `--filter
141
- field=operation:value` (repeatable, AND-combined; the operation defaults to
142
- `equals`) and `--sort field:asc|desc`; the accepted fields are listed in
143
- `--help` and an unknown one fails with `E_VALIDATION` before any request.
144
- `events list` takes its filters as named flags (`--category`, `--actor-id`,
145
- `--created-after`, …).
146
- - `--fields id,attributes.email` chooses which columns the human rendering
147
- shows (dot-paths allowed); an unknown path fails before any request, with the
148
- available ones in `details.available`; a user's schema-defined `attributes`
149
- accept any key. `--json` always carries the whole resource regardless.
150
- - `get <id>` emits the resource as `data`. Its human rendering lays the record
151
- out field by field on a terminal and prints the whole object when piped;
152
- `--json` is unchanged either way.
153
- - `create` / `update <id>` take the body either as one flag per schema field
154
- (`--name`, `--principal-type`, …; run `<resource> create --help` for the
155
- list, where required fields are marked `(required)`) or as a whole JSON
156
- object via `--data '<json>'` / `--file <path>` (`--file -` reads stdin). A
157
- field flag overrides the same key in `--data`. A user's schema-defined
158
- `attributes` are set with the repeatable `--attributes`, where `key=value` is
159
- always a string and `key:=value` parses the value as JSON — use `:=` for a
160
- field the user schema types as a number, boolean, null, array, or object
161
- (`--attributes age:=42`), and `=` to keep a numeric-looking identifier a
162
- string (`--attributes postcode=02139`). A missing
163
- required field fails with `E_VALIDATION`, naming the flags in `message` and
164
- listing their wire names in `details.missing`. The body is validated against
165
- the API schema locally, and the server's resource is emitted as `data`. A create adds
166
- `data.next_commands` pointing at the matching `get`. `--dry-run` emits
167
- `{ dry_run: true, verb, topic, body }` without calling the platform.
168
- - The destructive verb (`delete`, `revoke`, `deactivate`) requires `--force` in non-interactive mode (declared per command, so its help says what it permits)
169
- (the error's `next_commands` carries the exact retry) and report what the API
170
- did: `{ id, deleted: true }` for users and grants, `{ id, revoked: true }` for
171
- sessions, and `{ id, deactivated: true }` for teams, whose DELETE deactivates
172
- the team and leaves it readable (ADR 024). Read the property that accompanies
173
- `id` rather than assuming `deleted`.
174
-
175
- ```sh
176
- npx @zitadel/cli@alpha users list --filter status=active --sort created_at:desc --non-interactive --json
177
- npx @zitadel/cli@alpha users create --schema sch_… --attributes email=a@b.c --non-interactive --json
178
- npx @zitadel/cli@alpha sessions revoke sess_… --force --non-interactive --json
179
- ```
180
-
181
- ## Commands
182
-
183
- The groups below mirror the ones `zitadel --help` prints.
184
-
185
- ### Project commands
186
-
187
- - `setup` — create a Zitadel project and scaffold local auth (routes,
188
- middleware, `.zitadel/**`, env templates). Setup writes the versioned local
189
- default user schema and login flow into
190
- `.zitadel/schemas/default-human-user.json` and
191
- `.zitadel/flows/default-login.json`, uploads them through the schema and flow
192
- APIs, then seeds `.zitadel/state.json` so `plan` is immediately empty. Agents
193
- must pass `--framework` when scaffolding into a fresh directory; interactive
194
- humans can omit it and choose from the prompt. Supported floors: Next.js 15+
195
- and React 18+ — `setup` and `doctor` fail with `E_UNSUPPORTED_PROJECT_SHAPE`
196
- below them instead of degrading silently (an unparseable version passes).
197
- Flags:
198
- `--framework next|react|vue|angular|nuxt|solid|svelte|qwik`, `--renderer
199
- react` (selects the Next.js auth-page renderer; accepted for any framework
200
- and recorded in `zitadel.json` branding, but only Next varies its generated
201
- templates by it; the planned `web-component` renderer is not yet available
202
- and is rejected if passed), `--dev-port` (dev-server port, also the issuer
203
- origin registered with Zitadel — use distinct ports to run several scaffolded
204
- apps side by side. The app must actually serve this port or the flow API
205
- rejects its origin on the first submit, so setup makes the port explicit in
206
- the app's own dev-server config: `server.port` + `strictPort` for Vite
207
- frameworks, `serve.options.port` for Angular, and — because `next dev` and
208
- `nuxt dev` take a port only from the command line — `--port` in the
209
- `package.json` `dev` script for Next and Nuxt. On a pre-existing app that
210
- means setup edits the `dev` script when it does not already name that port;
211
- a script already on it is left untouched), `--preset password-first|passkey-first` (the sign-in
212
- experience the scaffold starts from: `password-first` is the default —
213
- email + password with passkey optional during registration; `passkey-first`
214
- enters login on a one-tap passkey step with an email + password fallback;
215
- recorded in `zitadel.json`), `--use-case minimal|consumer|business` (which
216
- profile fields the scaffolded schema collects: `minimal` is the default —
217
- email only; `consumer` adds given and family name; `business` also adds a
218
- `companyName` attribute and overlays work-email copy on the generated auth
219
- pages via the SDK's `businessLocales`; asked before `--preset` and recorded
220
- in `zitadel.json`), `--design centered|split|split-right|hero|minimal`
221
- (starter login design: ejects the design's template into
222
- `.zitadel/branding/` and publishes it as branding revision 1 during setup;
223
- the interactive wizard asks this as its final question with the built-in
224
- template preselected — omit the flag in non-interactive runs to keep the
225
- built-in template and no branding files), `--skip-install`.
226
- On Next and Nuxt, the scaffolded auth/profile pages derive their embedding
227
- posture from the app: a fresh scaffold (setup created the skeleton) pins
228
- `variant="page"` full-page chrome, while a pre-existing app embeds
229
- `variant="widget"` cards with `theme="auto"` in a layout-neutral wrapper.
230
- `theme="auto"` follows the OS `prefers-color-scheme`, not the host app's
231
- own theme — edit the generated page to set `theme="light"` or
232
- `theme="dark"` when the app pins its scheme. Other frameworks always
233
- scaffold the page posture. The chosen posture is recorded in the scaffold
234
- manifest, `doctor --fix` restores managed pages in the recorded posture,
235
- and editing the generated page is the supported way to change presentation
236
- — there is no config knob.
237
- Widget-posture embedding levers: host-page CSS sets `--zl-*` design-token
238
- custom properties on the element to bridge the app's look through the
239
- widget's shadow DOM (fonts `--zl-font-family-heading`/`-sans`, radii
240
- `--zl-radius-*`, primary CTA `--zl-primary`/`--zl-primary-foreground`,
241
- link color `--zl-link`); the `suppress-header` attribute
242
- (wrapper prop `suppressHeader`) visually hides the widget's own heading
243
- block when the page already carries one, keeping it in the accessibility
244
- tree. Split-family designs collapse their brand pane by container width —
245
- at card width they show the compact brand mark (`logo_url`, else
246
- `hero_url`, from `.zitadel/branding/branding.json`; `hero` falls back to
247
- editable text), and setup warns when a widget-posture app picks `split`
248
- or `split-right`.
249
- - `claim` — claim the project for a team to make it permanent. Mints a
250
- short-lived link, opens it in a browser, and blocks until the developer
251
- finishes signing in there, then records `claimed_at` and `team_id` in
252
- `.zitadel/secret`. Nothing about the project changes: the issuer, users,
253
- passkeys, and applications keep working, and the project secret is not
254
- rotated. Re-running once the project belongs to a team is a clean
255
- `status: "skipped"` with `reason: "already-claimed"`, so agents can retry
256
- safely. The link is always printed before any browser opens, so a headless
257
- machine, an SSH session, or `--no-open` needs no special handling — copy it
258
- and open it anywhere. Links last 10 minutes; once one lapses the command
259
- exits `E_VALIDATION` and points at a fresh run. Claiming itself is only
260
- possible within 14 days of project creation: past that the platform answers
261
- `410 proj.claim_window_expired`, the command exits `E_VALIDATION`, and a
262
- fresh link does **not** help — only a fresh `setup` yields a claimable
263
- project (the old one can no longer be claimed; it stays temporary and its
264
- data may be lost).
265
- `--dry-run` stops before
266
- anything is minted and reports `status: "skipped"`, `reason: "dry-run"` —
267
- there is nothing to preview, because a claim is decided in a browser.
268
- Flags: `--no-open` (print the link instead of launching a browser),
269
- `--timeout <seconds>` (stop waiting sooner than the link's own expiry).
270
- `setup`, `status`, and `doctor` report whether a team is attached, reading
271
- `claimed_at`/`team_id` from `.zitadel/secret` (no platform call). `status`
272
- carries `data.project.claim` as
273
- `{"kind": "detached", "claimable": true, "deadline": "2026-09-18T09:00:00.000Z"}`
274
- (`claimable` flips to `false` once the locally recorded creation time says
275
- the 14-day window has passed; the guidance then switches to reconciliation
276
- wording but the claim command stays in `next_commands`, because the local
277
- record can be stale and running `claim` answers authoritatively — an
278
- attached project skips cleanly. `deadline` is omitted when the creation
279
- time is unknown) or
280
- `{"kind": "attached", "team_id": "team_01H…", "claimed_at": "2026-08-01T09:00:00.000Z"}`,
281
- and `doctor` reports a
282
- `claim` check. A project with no team is only ever a **warning**, never a
283
- failure — it works exactly like one with a team, so `doctor` still exits 0
284
- and `--fix` deliberately does nothing (a claim needs a human in a browser).
285
- `status` and `doctor` stay silent about teams off the cloud: they answer
286
- offline and cannot know whether a local or self-hosted server hosts a
287
- platform to claim into. `setup` is online anyway, so against a local server
288
- it probes the runtime document and nudges only when the server hosts the
289
- platform plane (`platform.bootstrap_project`), where the claim can actually
290
- complete.
291
- - `doctor` — verify generated app files and local state once `zitadel.json`
292
- exists. The `managed-files` check compares the scaffolded app files against
293
- the manifest setup recorded in `.zitadel/state.json`: a missing
294
- infrastructure file (the request boundary, `custom-elements.d.ts`) fails,
295
- a missing generated page warns, and files you edited (marker kept) or
296
- replaced (marker removed) pass as `edited`/`adopted`. It also verifies the
297
- managed config wirings (Vite/Nuxt proxy merges, Angular's `angular.json`
298
- proxy and auth routes) through the patchers' idempotent transforms — a
299
- detached or missing wiring config fails, an unverifiable one warns, and
300
- `--fix` re-applies it. The Next/Nuxt `dev` script is verified the same way,
301
- against the port recorded as the development issuer rather than the port the
302
- script names today: a script moved off that port reports as an unapplied
303
- config edit (a warning — a `dev` script is not the only way to choose a
304
- port), and `--fix` restores the registered one. Boundary migrations converge: a pristine leftover
305
- `middleware.ts` from a Next 15→16 upgrade is swapped for `proxy.ts`, while
306
- an edited one is reported as a conflict instead of creating both (Next
307
- rejects the pair). The default local
308
- runtime is the `@zitadel/server` npm binary; Docker checks apply only when
309
- using `--runtime docker` or `--image`. `--fix` restores missing managed
310
- files and never replaces an existing scaffolded app file; additive repairs
311
- (missing `.gitignore` entries, `.env.example` keys) still append to their
312
- targets, and the SDK dependency is re-added only when absent — an existing
313
- version pin is never rewritten. The `dependency-version` check warns when
314
- an exactly-pinned `@zitadel/*` dependency does not match the CLI's own
315
- version (the packages release as one train, and a floating
316
- `npx @zitadel/cli@alpha` can run ahead of the app's pins); ranges,
317
- dist-tags, and `file:`/`workspace:` specifiers express a deliberate choice
318
- and are not compared. The repair — an exact-pin install command for the
319
- project's detected package manager — is emitted in `data.next_commands`
320
- and quoted in the warning message.
321
- - `eject` (alias `uninstall`) — remove managed files and local Zitadel state;
322
- requires `--force` when non-interactive.
323
-
324
- ### Local server commands
325
-
326
- - `start` — start the managed local Zitadel server and persist runtime metadata
327
- under `.zitadel/local/runtime.json`. The binary runtime defaults to SQLite
328
- under `.zitadel/local/nextgen-data/`. Runtime metadata reports the published
329
- server package version; the repository contributor wrapper reports the
330
- `dev+<short-commit>` source build it launched. That label names the revision
331
- the binary was built from, which after a Moon cache hit can be an earlier
332
- commit whose server sources are byte-identical. Use `--runtime docker` or
333
- `--image` for the Docker backend. The project's env files configure the
334
- local server: every `NEXTGEN_*` variable in `.env.local` and `.env` (the
335
- former wins; empty values are skipped) is handed to the runtime through its
336
- environment only (bare `--env NAME` on Docker), so no value reaches `argv`,
337
- logs, `runtime.json`, or `--json`. The address, data dir and public base the
338
- CLI sets itself always win. `data.runtime.env` and `runtime.json` carry
339
- `injected`, the list of names. A running runtime is not updated in place:
340
- after changing a value run `stop` then `start`. An unreadable env file fails
341
- `start` with `E_VALIDATION` before any runtime is stopped. `setup` writes a
342
- comment saying so at the top of the scaffolded `.env.example` and
343
- `.env.local`.
344
- The server boots with the platform project
345
- and a local admin, so the developer exists on their own server without
346
- signing up: the admin signs in as `admin@zitadel.localhost`, and a
347
- generated password is kept in `.zitadel/local/admin.json` (gitignored with
348
- the rest of `.zitadel/local/`) and never printed. `start` prints a one-time
349
- console sign-in link and reports it as `data.console.sign_in_url` with
350
- `data.console.signed_in_as`; if no link can be minted (for example a data
351
- directory from before the local admin existed), `data.console.error` and
352
- `data.console.hint` say why, `start` still succeeds, and `zitadel console`
353
- drops out of `next_commands`. Setting `NEXTGEN_PLATFORM_BOOTSTRAP_PROJECT=false`,
354
- in the shell or in `.env.local` / `.env`, opts out of both the platform project
355
- and the local admin, for harnesses that
356
- want a bare single-project server; `data.console` is then absent.
357
- - `console` — print (and, interactively, open) a fresh one-time sign-in link
358
- for the local console as the local admin: `data.sign_in_url`,
359
- `data.signed_in_as`, `data.browser_opened`. Each link works once; run the
360
- command again for a new one. The console honours the link only when it is
361
- served from loopback, since the token signs in whoever opens it. Fails with `E_VALIDATION` when `start` never
362
- created a local admin in this directory. Flags: `--no-open`.
363
- - `stop` — stop the managed runtime while preserving
364
- `.zitadel/local/nextgen-data`. Use `stop --all` to sweep all discovered
365
- host-wide CLI-managed local runtime processes, including healthy runtimes
366
- from other local projects; it does not kill arbitrary `/healthz` listeners.
367
- - `status` — summarize the local runtime and project state.
368
- `data.server.runtime.env.injected` repeats the variable names recorded at
369
- the last `start` (empty for a runtime started before this field existed).
370
- - `logs` — print managed runtime logs; `--follow` streams in human mode.
371
- - `reset` — stop/remove the managed runtime and delete local runtime data;
372
- requires `--force` when non-interactive.
373
-
374
- Alpha releases are fixed product package trains. `npx @zitadel/cli@alpha start`
375
- uses the matching `@zitadel/server` package by default. `zitadel start --runtime
376
- docker --image <ref>` remains the explicit image override for debugging.
377
-
378
- ### Configuration commands
379
-
380
- - `plan` — validate config and preview the sync diff without mutating anything.
381
- - `apply` — validate and upload repo config to the platform.
382
- - `plan` and `apply --dry-run` also emit `data.warnings`: non-blocking
383
- findings as `{path, rule, message}`, the same text the human plan prints as
384
- `# warning:` lines and `apply` prints through stderr. They never fail a run.
385
- Two families exist today: flow-definition rules (`warn/…`, mirrored from the
386
- server's validator) and branding asset reachability. `warn/asset-unreachable`
387
- and `warn/asset-content-type` come from a bounded HEAD probe of
388
- `logo_url` / `hero_url` — a URL that is well-formed but dead passes every
389
- gate and then renders as a 0×0 image with nothing in the console, so the
390
- probe is the only place it can be caught. It is advisory by design: the
391
- machine planning is not necessarily the machine that renders the login page.
392
- The probe only contacts public HTTPS destinations and re-checks every
393
- redirect; loopback/private/internal targets stay inconclusive instead of
394
- turning repo config into a network request from the planning host.
395
- Set `ZITADEL_SKIP_ASSET_PROBE` to turn it off (offline, air-gapped CI, or a
396
- CDN that only resolves from production) and `ZITADEL_ASSET_PROBE_TIMEOUT_MS`
397
- to retune the per-URL budget (default 2500).
398
- - In the human-readable plan, a multi-line field (branding's inlined
399
- `liquid_template`) renders as `(<n> lines, sha256:…)` when it is created or
400
- unchanged, and as a changed-line diff when it moved — not as one escaped
401
- line. Read the file itself for full content.
402
- - `schemas list` — inspect the revision history of a user-schema, filtered by
403
- `--object-type` (e.g. `human-user`). Non-interactive/`--json` prints one row
404
- per revision (newest first); interactive adds a picker that fetches and
405
- pretty-prints the selected revision body.
406
- - `branding eject` — take ownership of the login template: scaffold
407
- `.zitadel/branding/` (a `branding.json` descriptor plus the `login.liquid`
408
- template) from a shipped design, `--design centered|split|split-right|hero|minimal`
409
- or an interactive picker on a TTY. `plan`/`apply` then publish every edit as
410
- a new branding revision.
411
- - `variables list|get|set|delete` — manage the per-environment variables and
412
- secrets a configuration document references as `${{ NAME }}`. Every command
413
- addresses one owner: `--environment <name>` (`-e`, alias `--env`) names an
414
- environment, and `--project-level` names the project level. Owners do not
415
- inherit from one another — a value set at the project level is **not** seen
416
- by any environment — so a value a running environment needs must be set on
417
- that environment, and one needed on several must be set on each. Because of
418
- that, the owner is never defaulted: with neither flag a person is asked and a
419
- non-interactive run fails with `E_VALIDATION` naming the project's
420
- environments, as ADR 035 specifies for `deploy`. `set` takes its value from a
421
- prompt or from stdin and never from a flag, so a credential never reaches
422
- `argv`; `--secret` stores it encrypted, after which it can be replaced but
423
- never read back (`list` reports it as held, and `--json` omits the value key
424
- entirely). `set --as number|boolean` stores a JSON number or boolean
425
- instead of a string, so a whole-field `${{ NAME }}` reference resolves to that
426
- type; it is refused with `--secret`, and an integer too large to store exactly
427
- is refused rather than rounded. Output follows the resource commands: on a
428
- pipe, `list` prints tab-separated `name`/`value` rows (a secret's value is
429
- `(secret)`) and `get` prints the whole record as JSON, which carries no
430
- `value` key for a secret. There is no `pull` and no bulk import. `set` and
431
- `delete` honour `--dry-run` and make no change (with no owner flag, a person is still asked,
432
- which reads the project's environments); `delete` needs `--force` when
433
- non-interactive.
434
-
435
- ## Golden path
436
-
437
- ```sh
438
- npx @zitadel/cli@alpha doctor --non-interactive --json
439
- npx @zitadel/cli@alpha start --non-interactive --json
440
- npx @zitadel/cli@alpha setup --framework next --server local --non-interactive --json
441
- npx @zitadel/cli@alpha doctor --non-interactive --json
442
- npx @zitadel/cli@alpha plan --non-interactive --json
443
- npx @zitadel/cli@alpha apply --non-interactive --json
444
- ```
445
-
446
- Exact alpha train invocation:
447
-
448
- ```sh
449
- npx @zitadel/cli@0.1.0-alpha.N doctor --non-interactive --json
450
- npx @zitadel/cli@0.1.0-alpha.N start --non-interactive --json
451
- npx @zitadel/cli@0.1.0-alpha.N setup --framework next --server local --non-interactive --json
452
- ```
453
-
454
- After `setup`, follow `data.next_commands` to start the app. Prove the generated
455
- auth flow in a visible browser by registering a unique user, logging out, logging
456
- back in with the same email/password, and ending on the signed-in profile page.
457
- Do not treat a rendered login or registration form as completion.
458
-
459
- ### Driving the login UI
460
-
461
- `<zitadel-login>` and `<zitadel-logout>` are Lit elements with open shadow
462
- roots. The stable automation hooks live inside nested shadow roots, so a flat
463
- `document.querySelector('[data-testid="zitadel-input-email"]')` will not find
464
- the native control. Browser drivers with shadow-DOM-aware locators, such as
465
- Playwright, can target the hooks directly. Generic DOM-eval drivers should
466
- pierce shadow roots recursively:
467
-
468
- ```js
469
- function deepQuery(sel, root = document) {
470
- const hit = root.querySelector(sel);
471
- if (hit) return hit;
472
- for (const el of root.querySelectorAll("*")) {
473
- if (el.shadowRoot) {
474
- const result = deepQuery(sel, el.shadowRoot);
475
- if (result) return result;
476
- }
477
- }
478
- return null;
479
- }
480
- ```
481
-
482
- Use host hooks such as `zitadel-field-email`, `zitadel-field-password`, and
483
- `zitadel-action-submit` when targeting the Lit atoms. Use native shadow-control
484
- hooks such as `zitadel-input-email`, `zitadel-input-password`, and
485
- `zitadel-action-submit-button` when filling or clicking the underlying input or
486
- button. Hooks stay method-named even when the flow engine names a credential
487
- field `x-auth-methods#<method>`; only the `name` attribute carries that raw
488
- form key. Enter inside a field submits the step's primary action, but only for
489
- key events that carry `key: "Enter"` — drivers whose synthesized key events
490
- omit it (some CDP wrappers) should click `zitadel-action-submit` instead. For sign-out, open the user menu button if needed, then pierce to
491
- `.signout-btn`; Playwright-style locators may use `zitadel-logout .signout-btn`.
492
- The canonical component hook list lives in `packages/components/README.md`.
493
-
494
- The checked-in automated regression path is `moon run workspace:journey`, which
495
- exercises fresh-app setup plus registration, logout, and login across the
496
- supported frameworks.
497
-
498
- Repo config is authoritative: edit `zitadel.json` or files under `.zitadel/`,
499
- then re-run `plan` and `apply`. Schema and flow files are synced from
500
- `.zitadel/schemas/*.json` and `.zitadel/flows/*.json`. Login templates
501
- (branding) are synced from `.zitadel/branding/`: a single `branding.json`
502
- descriptor (layout, asset URLs) plus a sibling `login.liquid` LiquidJS
503
- template referenced as `"liquid_template": { "$file": "./login.liquid" }`. Scaffold them with the
504
- `branding eject` command (`--design centered|split|split-right|hero|minimal`,
505
- interactive picker on a TTY) or at project creation with
506
- `setup --design <name>`, which also publishes revision 1. Branding is
507
- revisioned and immutable: every edit — including a `.liquid`-only edit —
508
- plans as a `revise` and `apply` publishes a new revision; the login serves
509
- the newest one. `plan` validates templates with the authoritative LiquidJS
510
- validator (`E_VALIDATION` lists rule ids such as `no-script-tag` and
511
- `mandatory-gates`; every template must keep a trailing
512
- `{% mandatory_gates %}` tag). `font_url` is not writable yet; asset URLs
513
- must be absolute `https://`. Keep exactly one descriptor in
514
- `.zitadel/branding/` — extra `*.json` files there fail the scan.
515
- Server-provisioned defaults remain a fallback for non-CLI project
516
- creation, but CLI-created projects are authored from local files first. Flows are
517
- revisioned like branding: an edit plans as a `revise` and `apply` publishes a new
518
- immutable flow revision; a schema revise re-publishes the flows pinned to it with
519
- the new `user_schema` in the same run. A login pinned by `flow-name` serves that
520
- flow's newest revision; an unpinned login serves the newest active unscoped flow
521
- in the project, whatever its name. Removing a flow file does not retire the
522
- flow. Managed files carry a marker comment; `eject` removes only
523
- files that still carry it, preserving anything the user replaced. For app-local
524
- development, `--server local` resolves through `.zitadel/local/runtime.json` and
525
- requires a healthy
526
- `npx @zitadel/cli@alpha start` runtime. Runtime-only `.zitadel/local/**` state
527
- does not block fresh same-directory scaffolding. `setup` installs dependencies
528
- with the detected package manager by default; pass `--skip-install` when the
529
- agent or host workflow will install dependencies separately.