@zitadel/cli 1.0.0-alpha.24 → 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,536 +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` never writes `.zitadel/branding/` or publishes a branding
61
- revision: the login renders the maintained `<zitadel-login>` component.
62
- Taking ownership of the widget template is the separate, opt-in
63
- `branding eject` command.
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`, `--relation`, …; 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.
193
- Against a local server that hosts the platform project and has a local admin
194
- (the default `start` configuration), setup also attaches the project to the
195
- local admin's team: it writes `team_id` and `claimed_at` into
196
- `.zitadel/secret` and prints
197
- `Project owned by admin@zitadel.localhost (team ...)`, so a later `claim`
198
- returns `status: "skipped"` with `reason: "already-claimed"`. The step is
199
- best-effort. When the attempt fails on a platform-hosting runtime, setup
200
- warns and the normal claim nudge applies. When `start` opted out of the
201
- platform bootstrap there is no local admin and no claim surface, so setup
202
- skips the step silently, emits no nudge, and the project has no owning
203
- team. Agents
204
- must pass `--framework` when scaffolding into a fresh directory; interactive
205
- humans can omit it and choose from the prompt. Supported floors: Next.js 15+
206
- and React 18+ — `setup` and `doctor` fail with `E_UNSUPPORTED_PROJECT_SHAPE`
207
- below them instead of degrading silently (an unparseable version passes).
208
- Flags:
209
- `--framework next|react|vue|angular|nuxt|solid|svelte|qwik`, `--renderer
210
- react` (selects the Next.js auth-page renderer; accepted for any framework
211
- and recorded in `zitadel.json` branding, but only Next varies its generated
212
- templates by it; the planned `web-component` renderer is not yet available
213
- and is rejected if passed), `--dev-port` (dev-server port, also the issuer
214
- origin registered with Zitadel — use distinct ports to run several scaffolded
215
- apps side by side. The app must actually serve this port or the flow API
216
- rejects its origin on the first submit, so setup makes the port explicit in
217
- the app's own dev-server config: `server.port` + `strictPort` for Vite
218
- frameworks, `serve.options.port` for Angular, and — because `next dev` and
219
- `nuxt dev` take a port only from the command line — `--port` in the
220
- `package.json` `dev` script for Next and Nuxt. On a pre-existing app that
221
- means setup edits the `dev` script when it does not already name that port;
222
- a script already on it is left untouched), `--preset password-first|passkey-first` (the sign-in
223
- experience the scaffold starts from: `password-first` is the default —
224
- email + password with passkey optional during registration; `passkey-first`
225
- enters login on a one-tap passkey step with an email + password fallback;
226
- recorded in `zitadel.json`), `--use-case minimal|consumer|business` (which
227
- profile fields the scaffolded schema collects: `minimal` is the default —
228
- email only; `consumer` adds given and family name; `business` also adds a
229
- `companyName` attribute and overlays work-email copy on the generated auth
230
- pages via the SDK's `businessLocales`; asked before `--preset` and recorded
231
- in `zitadel.json`), `--skip-install`. Setup does not ask about or apply a
232
- login design: it writes no `.zitadel/branding/` files and publishes no
233
- branding revision (`branding eject` is the opt-in for that).
234
- On Next and Nuxt, the scaffolded auth/profile pages derive their embedding
235
- posture from the app: a fresh scaffold (setup created the skeleton) pins
236
- `variant="page"` full-page chrome, while a pre-existing app embeds
237
- `variant="widget"` cards with `theme="auto"` in a layout-neutral wrapper.
238
- `theme="auto"` follows the OS `prefers-color-scheme`, not the host app's
239
- own theme — edit the generated page to set `theme="light"` or
240
- `theme="dark"` when the app pins its scheme. Other frameworks always
241
- scaffold the page posture. The chosen posture is recorded in the scaffold
242
- manifest, `doctor --fix` restores managed pages in the recorded posture,
243
- and editing the generated page is the supported way to change presentation
244
- — there is no config knob.
245
- Widget-posture embedding levers: host-page CSS sets `--zl-*` design-token
246
- custom properties on the element to bridge the app's look through the
247
- widget's shadow DOM (fonts `--zl-font-family-heading`/`-sans`, radii
248
- `--zl-radius-*`, primary CTA `--zl-primary`/`--zl-primary-foreground`,
249
- link color `--zl-link`); the `suppress-header` attribute
250
- (wrapper prop `suppressHeader`) visually hides the widget's own heading
251
- block when the page already carries one, keeping it in the accessibility
252
- tree. Page layout around the widget (split screens, hero panes) is the
253
- app's own code, not a Zitadel template.
254
- - `claim` — claim the project for a team to make it permanent. Mints a
255
- short-lived link, opens it in a browser, and blocks until the developer
256
- finishes signing in there, then records `claimed_at` and `team_id` in
257
- `.zitadel/secret`. Nothing about the project changes: the issuer, users,
258
- passkeys, and applications keep working, and the project secret is not
259
- rotated. Re-running once the project belongs to a team is a clean
260
- `status: "skipped"` with `reason: "already-claimed"`, so agents can retry
261
- safely. The link is always printed before any browser opens, so a headless
262
- machine, an SSH session, or `--no-open` needs no special handling — copy it
263
- and open it anywhere. Links last 10 minutes; once one lapses the command
264
- exits `E_VALIDATION` and points at a fresh run. Claiming itself is only
265
- possible within 14 days of project creation: past that the platform answers
266
- `410 proj.claim_window_expired`, the command exits `E_VALIDATION`, and a
267
- fresh link does **not** help — only a fresh `setup` yields a claimable
268
- project (the old one can no longer be claimed; it stays temporary and its
269
- data may be lost).
270
- `--dry-run` stops before
271
- anything is minted and reports `status: "skipped"`, `reason: "dry-run"` —
272
- there is nothing to preview, because a claim is decided in a browser.
273
- Flags: `--no-open` (print the link instead of launching a browser),
274
- `--timeout <seconds>` (stop waiting sooner than the link's own expiry).
275
- `setup`, `status`, and `doctor` report whether a team is attached, reading
276
- `claimed_at`/`team_id` from `.zitadel/secret` (no platform call). `status`
277
- carries `data.project.claim` as
278
- `{"kind": "detached", "claimable": true, "deadline": "2026-09-18T09:00:00.000Z"}`
279
- (`claimable` flips to `false` once the locally recorded creation time says
280
- the 14-day window has passed; the guidance then switches to reconciliation
281
- wording but the claim command stays in `next_commands`, because the local
282
- record can be stale and running `claim` answers authoritatively — an
283
- attached project skips cleanly. `deadline` is omitted when the creation
284
- time is unknown) or
285
- `{"kind": "attached", "team_id": "team_01H…", "claimed_at": "2026-08-01T09:00:00.000Z"}`,
286
- and `doctor` reports a
287
- `claim` check. A project with no team is only ever a **warning**, never a
288
- failure — it works exactly like one with a team, so `doctor` still exits 0
289
- and `--fix` deliberately does nothing (a claim needs a human in a browser).
290
- `status` and `doctor` stay silent about teams off the cloud: they answer
291
- offline and cannot know whether a local or self-hosted server hosts a
292
- platform to claim into. `setup` is online anyway, so against a local server
293
- it probes the runtime document and nudges only when the server hosts the
294
- platform plane (`platform.bootstrap_project`), where the claim can actually
295
- complete.
296
- - `doctor` — verify generated app files and local state once `zitadel.json`
297
- exists. The `managed-files` check compares the scaffolded app files against
298
- the manifest setup recorded in `.zitadel/state.json`: a missing
299
- infrastructure file (the request boundary, `custom-elements.d.ts`) fails,
300
- a missing generated page warns, and files you edited (marker kept) or
301
- replaced (marker removed) pass as `edited`/`adopted`. It also verifies the
302
- managed config wirings (Vite/Nuxt proxy merges, Angular's `angular.json`
303
- proxy and auth routes) through the patchers' idempotent transforms — a
304
- detached or missing wiring config fails, an unverifiable one warns, and
305
- `--fix` re-applies it. The Next/Nuxt `dev` script is verified the same way,
306
- against the port recorded as the development issuer rather than the port the
307
- script names today: a script moved off that port reports as an unapplied
308
- config edit (a warning — a `dev` script is not the only way to choose a
309
- port), and `--fix` restores the registered one. Boundary migrations converge: a pristine leftover
310
- `middleware.ts` from a Next 15→16 upgrade is swapped for `proxy.ts`, while
311
- an edited one is reported as a conflict instead of creating both (Next
312
- rejects the pair). The default local
313
- runtime is the `@zitadel/server` npm binary; Docker checks apply only when
314
- using `--runtime docker` or `--image`. `--fix` restores missing managed
315
- files and never replaces an existing scaffolded app file; additive repairs
316
- (missing `.gitignore` entries, `.env.example` keys) still append to their
317
- targets, and the SDK dependency is re-added only when absent — an existing
318
- version pin is never rewritten. The `dependency-version` check warns when
319
- an exactly-pinned `@zitadel/*` dependency does not match the CLI's own
320
- version (the packages release as one train, and a floating
321
- `npx @zitadel/cli@alpha` can run ahead of the app's pins); ranges,
322
- dist-tags, and `file:`/`workspace:` specifiers express a deliberate choice
323
- and are not compared. The repair — an exact-pin install command for the
324
- project's detected package manager — is emitted in `data.next_commands`
325
- and quoted in the warning message.
326
- - `eject` (alias `uninstall`) — remove managed files and local Zitadel state;
327
- requires `--force` when non-interactive.
328
-
329
- ### Local server commands
330
-
331
- - `start` — start the managed local Zitadel server and persist runtime metadata
332
- under `.zitadel/local/runtime.json`. The binary runtime defaults to SQLite
333
- under `.zitadel/local/nextgen-data/`. Runtime metadata reports the published
334
- server package version; the repository contributor wrapper reports the
335
- `dev+<short-commit>` source build it launched. That label names the revision
336
- the binary was built from, which after a Moon cache hit can be an earlier
337
- commit whose server sources are byte-identical. Use `--runtime docker` or
338
- `--image` for the Docker backend. The project's env files configure the
339
- local server: every `NEXTGEN_*` variable in `.env.local` and `.env` (the
340
- former wins; empty values are skipped) is handed to the runtime through its
341
- environment only (bare `--env NAME` on Docker), so no value reaches `argv`,
342
- logs, `runtime.json`, or `--json`. The address, data dir and public base the
343
- CLI sets itself always win. `data.runtime.env` and `runtime.json` carry
344
- `injected`, the list of names. A running runtime is not updated in place:
345
- after changing a value run `stop` then `start`. An unreadable env file fails
346
- `start` with `E_VALIDATION` before any runtime is stopped. `setup` writes a
347
- comment saying so at the top of the scaffolded `.env.example` and
348
- `.env.local`.
349
- The server boots with the platform project
350
- and a local admin, so the developer exists on their own server without
351
- signing up: the admin signs in as `admin@zitadel.localhost`, and a
352
- generated password is kept in `.zitadel/local/admin.json` (gitignored with
353
- the rest of `.zitadel/local/`) and never printed. `start` prints a one-time
354
- console sign-in link and reports it as `data.console.sign_in_url` with
355
- `data.console.signed_in_as`; if no link can be minted (for example a data
356
- directory from before the local admin existed), `data.console.error` and
357
- `data.console.hint` say why, `start` still succeeds, and `zitadel console`
358
- drops out of `next_commands`. Setting `NEXTGEN_PLATFORM_BOOTSTRAP_PROJECT=false`,
359
- in the shell or in `.env.local` / `.env`, opts out of both the platform project
360
- and the local admin, for harnesses that
361
- want a bare single-project server; `data.console` is then absent.
362
- - `console` — print (and, interactively, open) a fresh one-time sign-in link
363
- for the local console as the local admin: `data.sign_in_url`,
364
- `data.signed_in_as`, `data.browser_opened`. Each link works once; run the
365
- command again for a new one. The console honours the link only when it is
366
- served from loopback, since the token signs in whoever opens it. Fails with `E_VALIDATION` when `start` never
367
- created a local admin in this directory. Flags: `--no-open`.
368
- - `stop` — stop the managed runtime while preserving
369
- `.zitadel/local/nextgen-data`. Use `stop --all` to sweep all discovered
370
- host-wide CLI-managed local runtime processes, including healthy runtimes
371
- from other local projects; it does not kill arbitrary `/healthz` listeners.
372
- - `status` — summarize the local runtime and project state.
373
- `data.server.runtime.env.injected` repeats the variable names recorded at
374
- the last `start` (empty for a runtime started before this field existed).
375
- - `logs` — print managed runtime logs; `--follow` streams in human mode.
376
- - `reset` — stop/remove the managed runtime and delete local runtime data;
377
- requires `--force` when non-interactive.
378
-
379
- Alpha releases are fixed product package trains. `npx @zitadel/cli@alpha start`
380
- uses the matching `@zitadel/server` package by default. `zitadel start --runtime
381
- docker --image <ref>` remains the explicit image override for debugging.
382
-
383
- ### Configuration commands
384
-
385
- - `plan` — validate config and preview the sync diff without mutating anything.
386
- - `apply` — validate and upload repo config to the platform.
387
- - `plan` and `apply --dry-run` also emit `data.warnings`: non-blocking
388
- findings as `{path, rule, message}`, the same text the human plan prints as
389
- `# warning:` lines and `apply` prints through stderr. They never fail a run.
390
- Two families exist today: flow-definition rules (`warn/…`, mirrored from the
391
- server's validator) and branding asset reachability. `warn/asset-unreachable`
392
- and `warn/asset-content-type` come from a bounded HEAD probe of
393
- `logo_url` / `hero_url` — a URL that is well-formed but dead passes every
394
- gate and then renders as a 0×0 image with nothing in the console, so the
395
- probe is the only place it can be caught. It is advisory by design: the
396
- machine planning is not necessarily the machine that renders the login page.
397
- The probe only contacts public HTTPS destinations and re-checks every
398
- redirect; loopback/private/internal targets stay inconclusive instead of
399
- turning repo config into a network request from the planning host.
400
- Set `ZITADEL_SKIP_ASSET_PROBE` to turn it off (offline, air-gapped CI, or a
401
- CDN that only resolves from production) and `ZITADEL_ASSET_PROBE_TIMEOUT_MS`
402
- to retune the per-URL budget (default 2500).
403
- - In the human-readable plan, a multi-line field (branding's inlined
404
- `liquid_template`) renders as `(<n> lines, sha256:…)` when it is created or
405
- unchanged, and as a changed-line diff when it moved — not as one escaped
406
- line. Read the file itself for full content.
407
- - No command takes `--environment` (`-e`, `--env`). `plan`, `apply` and the
408
- resource commands work on the project's resources and never selected an
409
- environment; `variables` names its owner with `--project-level`. The flag
410
- returns when the platform's environments settle, and `deploy` (ADR 035) is
411
- what will put config onto one.
412
- - `schemas list` — inspect the revision history of a user-schema, filtered by
413
- `--object-type` (e.g. `human-user`). Non-interactive/`--json` prints one row
414
- per revision (newest first); interactive adds a picker that fetches and
415
- pretty-prints the selected revision body.
416
- - `branding eject` — take ownership of the login template: scaffold
417
- `.zitadel/branding/` (a `branding.json` descriptor plus the `login.liquid`
418
- template) from a shipped design, `--design centered|minimal` (the default card, or
419
- the same form without card chrome) or an interactive picker on a TTY. `plan`/`apply` then publish every edit as
420
- a new branding revision.
421
- - `variables list|get|set|delete` — manage the variables and secrets a
422
- configuration document references as `${{ NAME }}`. Every command addresses
423
- one owner, and `--project-level` is the only one the CLI can name today, so it
424
- is **required**: a run without it fails with `E_VALIDATION`, on a terminal as
425
- in a script. The platform also keeps variables per environment, but the CLI
426
- cannot address those until the platform's environments settle; `--environment`
427
- returns then, and every command written today keeps its meaning because the
428
- owner was named rather than assumed. Owners do not inherit from one another —
429
- a value entered at the project level is **not** seen by an environment
430
- (ADR 062 §4). `set` takes its value from a prompt or from stdin and never from
431
- a flag, so a credential never reaches `argv`; `--secret` stores it encrypted,
432
- after which it can be replaced but never read back (`list` reports it as held,
433
- and `--json` omits the value key entirely). `set --as number|boolean` stores a
434
- JSON number or boolean instead of a string, so a whole-field `${{ NAME }}`
435
- reference resolves to that type; it is refused with `--secret`, and an integer
436
- too large to store exactly is refused rather than rounded. Output follows the
437
- resource commands: on a pipe, `list` prints tab-separated `name`/`value` rows
438
- (a secret's value is `(secret)`) and `get` prints the whole record as JSON,
439
- which carries no `value` key for a secret. There is no `pull` and no bulk
440
- import. `set` and `delete` honour `--dry-run` and make no change; `delete`
441
- needs `--force` when non-interactive.
442
-
443
- ## Golden path
444
-
445
- ```sh
446
- npx @zitadel/cli@alpha doctor --non-interactive --json
447
- npx @zitadel/cli@alpha start --non-interactive --json
448
- npx @zitadel/cli@alpha setup --framework next --server local --non-interactive --json
449
- npx @zitadel/cli@alpha doctor --non-interactive --json
450
- npx @zitadel/cli@alpha plan --non-interactive --json
451
- npx @zitadel/cli@alpha apply --non-interactive --json
452
- ```
453
-
454
- Exact alpha train invocation:
455
-
456
- ```sh
457
- npx @zitadel/cli@0.1.0-alpha.N doctor --non-interactive --json
458
- npx @zitadel/cli@0.1.0-alpha.N start --non-interactive --json
459
- npx @zitadel/cli@0.1.0-alpha.N setup --framework next --server local --non-interactive --json
460
- ```
461
-
462
- After `setup`, follow `data.next_commands` to start the app. Prove the generated
463
- auth flow in a visible browser by registering a unique user, logging out, logging
464
- back in with the same email/password, and ending on the signed-in profile page.
465
- Do not treat a rendered login or registration form as completion.
466
-
467
- ### Driving the login UI
468
-
469
- `<zitadel-login>` and `<zitadel-logout>` are Lit elements with open shadow
470
- roots. The stable automation hooks live inside nested shadow roots, so a flat
471
- `document.querySelector('[data-testid="zitadel-input-email"]')` will not find
472
- the native control. Browser drivers with shadow-DOM-aware locators, such as
473
- Playwright, can target the hooks directly. Generic DOM-eval drivers should
474
- pierce shadow roots recursively:
475
-
476
- ```js
477
- function deepQuery(sel, root = document) {
478
- const hit = root.querySelector(sel);
479
- if (hit) return hit;
480
- for (const el of root.querySelectorAll("*")) {
481
- if (el.shadowRoot) {
482
- const result = deepQuery(sel, el.shadowRoot);
483
- if (result) return result;
484
- }
485
- }
486
- return null;
487
- }
488
- ```
489
-
490
- Use host hooks such as `zitadel-field-email`, `zitadel-field-password`, and
491
- `zitadel-action-submit` when targeting the Lit atoms. Use native shadow-control
492
- hooks such as `zitadel-input-email`, `zitadel-input-password`, and
493
- `zitadel-action-submit-button` when filling or clicking the underlying input or
494
- button. Hooks stay method-named even when the flow engine names a credential
495
- field `x-auth-methods#<method>`; only the `name` attribute carries that raw
496
- form key. Enter inside a field submits the step's primary action, but only for
497
- key events that carry `key: "Enter"` — drivers whose synthesized key events
498
- omit it (some CDP wrappers) should click `zitadel-action-submit` instead. For sign-out, open the user menu button if needed, then pierce to
499
- `.signout-btn`; Playwright-style locators may use `zitadel-logout .signout-btn`.
500
- The canonical component hook list lives in `packages/components/README.md`.
501
-
502
- The checked-in automated regression path is `moon run workspace:journey`, which
503
- exercises fresh-app setup plus registration, logout, and login across the
504
- supported frameworks.
505
-
506
- Repo config is authoritative: edit `zitadel.json` or files under `.zitadel/`,
507
- then re-run `plan` and `apply`. Schema and flow files are synced from
508
- `.zitadel/schemas/*.json` and `.zitadel/flows/*.json`. Login templates
509
- (branding) are synced from `.zitadel/branding/`: a single `branding.json`
510
- descriptor (layout, asset URLs) plus a sibling `login.liquid` LiquidJS
511
- template referenced as `"liquid_template": { "$file": "./login.liquid" }`. Scaffold them with the
512
- `branding eject` command (`--design centered|minimal`,
513
- interactive picker on a TTY); setup never scaffolds them. Branding is
514
- revisioned and immutable: every edit — including a `.liquid`-only edit —
515
- plans as a `revise` and `apply` publishes a new revision; the login serves
516
- the newest one. `plan` validates templates with the authoritative LiquidJS
517
- validator (`E_VALIDATION` lists rule ids such as `no-script-tag` and
518
- `mandatory-gates`; every template must keep a trailing
519
- `{% mandatory_gates %}` tag). `font_url` is not writable yet; asset URLs
520
- must be absolute `https://`. Keep exactly one descriptor in
521
- `.zitadel/branding/` — extra `*.json` files there fail the scan.
522
- Server-provisioned defaults remain a fallback for non-CLI project
523
- creation, but CLI-created projects are authored from local files first. Flows are
524
- revisioned like branding: an edit plans as a `revise` and `apply` publishes a new
525
- immutable flow revision; a schema revise re-publishes the flows pinned to it with
526
- the new `user_schema` in the same run. A login pinned by `flow-name` serves that
527
- flow's newest revision; an unpinned login serves the newest active unscoped flow
528
- in the project, whatever its name. Removing a flow file does not retire the
529
- flow. Managed files carry a marker comment; `eject` removes only
530
- files that still carry it, preserving anything the user replaced. For app-local
531
- development, `--server local` resolves through `.zitadel/local/runtime.json` and
532
- requires a healthy
533
- `npx @zitadel/cli@alpha start` runtime. Runtime-only `.zitadel/local/**` state
534
- does not block fresh same-directory scaffolding. `setup` installs dependencies
535
- with the detected package manager by default; pass `--skip-install` when the
536
- agent or host workflow will install dependencies separately.