@zitadel/cli 0.1.0-alpha.9 → 0.2.0

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.
Files changed (49) hide show
  1. package/README.md +4043 -229
  2. package/bin/run.js +8 -0
  3. package/dist/index.mjs +24173 -0
  4. package/dist/index.mjs.map +1 -0
  5. package/dist/lib/oclif/help.mjs +117 -0
  6. package/dist/lib/oclif/help.mjs.map +1 -0
  7. package/dist/skills-hint-CBoak9HL.mjs +4348 -0
  8. package/dist/skills-hint-CBoak9HL.mjs.map +1 -0
  9. package/oclif.manifest.json +8635 -253
  10. package/package.json +106 -15
  11. package/skills/zitadel-cli/SKILL.md +538 -0
  12. package/skills/zitadel-cli/references/driving-login-ui.md +109 -0
  13. package/SKILLS.md +0 -157
  14. package/dist/commands/apply.mjs +0 -62
  15. package/dist/commands/apply.mjs.map +0 -1
  16. package/dist/commands/doctor.mjs +0 -569
  17. package/dist/commands/doctor.mjs.map +0 -1
  18. package/dist/commands/eject.mjs +0 -145
  19. package/dist/commands/eject.mjs.map +0 -1
  20. package/dist/commands/logs.mjs +0 -58
  21. package/dist/commands/logs.mjs.map +0 -1
  22. package/dist/commands/plan.mjs +0 -52
  23. package/dist/commands/plan.mjs.map +0 -1
  24. package/dist/commands/reset.mjs +0 -79
  25. package/dist/commands/reset.mjs.map +0 -1
  26. package/dist/commands/setup.mjs +0 -856
  27. package/dist/commands/setup.mjs.map +0 -1
  28. package/dist/commands/start.mjs +0 -288
  29. package/dist/commands/start.mjs.map +0 -1
  30. package/dist/commands/status.mjs +0 -112
  31. package/dist/commands/status.mjs.map +0 -1
  32. package/dist/commands/stop.mjs +0 -105
  33. package/dist/commands/stop.mjs.map +0 -1
  34. package/dist/docker-CnGQK3ZK.mjs +0 -432
  35. package/dist/docker-CnGQK3ZK.mjs.map +0 -1
  36. package/dist/docker-guidance-ypN3IM3o.mjs +0 -21
  37. package/dist/docker-guidance-ypN3IM3o.mjs.map +0 -1
  38. package/dist/oclif-B7lBzh3R.mjs +0 -808
  39. package/dist/oclif-B7lBzh3R.mjs.map +0 -1
  40. package/dist/orca-U142Wrau.mjs +0 -2556
  41. package/dist/orca-U142Wrau.mjs.map +0 -1
  42. package/dist/ports-B09RjuHx.mjs +0 -111
  43. package/dist/ports-B09RjuHx.mjs.map +0 -1
  44. package/dist/processes-Cw8TO1SY.mjs +0 -120
  45. package/dist/processes-Cw8TO1SY.mjs.map +0 -1
  46. package/dist/project-Cd0L3PtM.mjs +0 -87
  47. package/dist/project-Cd0L3PtM.mjs.map +0 -1
  48. package/dist/sync-BojoQm2P.mjs +0 -733
  49. package/dist/sync-BojoQm2P.mjs.map +0 -1
@@ -0,0 +1,538 @@
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, Nuxt, SvelteKit, SolidStart, Qwik Router, TanStack Start, React,
8
+ Vue, Angular, Solid, Svelte, or Qwik app, or build and deploy Zitadel config
9
+ changes from repo state.
10
+ ---
11
+
12
+ # Zitadel CLI
13
+
14
+ The `zitadel` CLI integrates Zitadel auth into an existing project and keeps the
15
+ local config (`zitadel.json` and `.zitadel/**`) as the source of truth. Every
16
+ command emits a JSON envelope, so you should drive it non-interactively and
17
+ parse the result rather than scraping human output.
18
+
19
+ This skill teaches how to *drive* the CLI — how to invoke it, its output modes,
20
+ and its envelope. It deliberately does **not** enumerate the commands. The CLI
21
+ is self-describing, and this skill and the installed CLI version move
22
+ independently: a user may install the skill once and upgrade the CLI later, so
23
+ any command or flag list hardcoded here would drift out of lockstep and mislead.
24
+ Ask the CLI for its current surface (see [Discovering commands](#discovering-commands))
25
+ and treat what it prints as authoritative over anything you remember.
26
+
27
+ ## Installing and invoking
28
+
29
+ Run the CLI with `npx`, which fetches and caches it on demand — there is nothing
30
+ to install first:
31
+
32
+ ```sh
33
+ npx @zitadel/cli@latest <command> --non-interactive --json
34
+ ```
35
+
36
+ Use an exact version (`npx @zitadel/cli@0.1.0`) in agent scripts and bug
37
+ reports for reproducibility. The CLI is a preview: until 1.0.0 a minor version
38
+ can break, and its release note says so with **Breaking:**.
39
+ The CLI requires Node.js 24+. When `setup` creates a new app it first checks the running Node.js against the `engines.node` of the framework CLI it is about to run (`@angular/cli`, `nuxi`, `create-vite`, `create-next-app`). Only the Angular CLI refuses a Node it does not accept, and that range can be newer than 24.0: if `setup` refuses with `E_VALIDATION` naming the Node versions Angular accepts, switch to one of them and run it again. The Angular CLI checks each major on its own, so only 22.22.3+ on 22, 24.15.0+ on 24 and 26.x are accepted (23 and 25 run with a warning), and Node 27 or later is refused even though the published range reads `>=26.0.0`. For the other frameworks the CLI carries on, so `setup` adds a warning to the envelope's `warnings` and continues.
40
+
41
+ The app's framework must be at or above a minimum version: Next.js 15.2.3,
42
+ React 18, Vue 3.2, Nuxt 4, Angular 17, Solid 1.6, Svelte 5, Qwik 2.0.0-beta.45. Newer major
43
+ versions are not refused. `setup` and `doctor` refuse an app below its minimum,
44
+ by declared range or installed release, with `E_UNSUPPORTED_PROJECT_SHAPE`
45
+ (exit 3); `setup` refuses before it scaffolds or patches anything, while
46
+ `doctor --fix` can still repair unrelated files, so do not assume a floor error
47
+ left the project untouched. The message names the minimum and the hint says to
48
+ upgrade, so upgrade the app and rerun rather than retrying.
49
+
50
+ ## Using the CLI
51
+
52
+ - **Drive it non-interactively.** Always pass `--non-interactive --json` (short:
53
+ `-n`). The JSON object on stdout is the machine contract — parse it rather than
54
+ scraping human output. `--non-interactive` also disables every prompt, so a
55
+ command that would otherwise ask for an answer fails fast with `E_VALIDATION`
56
+ naming the flag to pass instead of blocking. Supply each answer as a flag; run
57
+ the command's `--help` to see which flags exist and which are required.
58
+ - **Output modes.** `--json` is the agent contract and is unaffected by the
59
+ terminal — color and width never apply to it, so you do **not** need
60
+ `--no-color` or other cosmetic flags when you parse JSON; don't sprinkle them
61
+ on every call. Human mode adapts to the terminal: a TTY gets aligned tables,
62
+ while a piped or redirected run (or `--plain`) gets one tab-separated record
63
+ per line and nothing else. If you ever read human output, set `NO_COLOR=1`
64
+ once in the environment rather than repeating `--no-color` per command.
65
+ - **Don't truncate `--json` with `head`/`tail`.** The envelope is one bounded
66
+ object; slicing it with `head -c`/`head -n` yields invalid JSON and silently
67
+ drops records or fields, so you end up reasoning from partial data. It is
68
+ finite — read it whole, or narrow it *at the source*: use a `list` verb's
69
+ `filter_fields` and pagination to fetch fewer records, or pipe to `jq` to pull
70
+ just what you need (e.g. `… resources --json | jq '.data.items[].topic'`).
71
+ `--help` output is bounded too — read it in full rather than `head`-ing it, or
72
+ you will miss flags further down.
73
+ - **Working directory.** Add `--cwd <path>` when operating outside the current
74
+ working directory.
75
+ - **Server and project.** A command that talks to a project reads
76
+ `ZITADEL_URL`, `ZITADEL_PROJECT_ID` and `ZITADEL_PROJECT_SECRET`, each from the
77
+ first source that has it: `--server` (the URL only), `--env-file <path>`, the
78
+ process environment, `.env.<env>.local`, `.env.local`, `.env.<env>`, `.env`,
79
+ then `.zitadel/secret`. Only the default `development` environment reads
80
+ `.env.local` and `.zitadel/secret` (setup's project); `--env-file` reads that
81
+ file and the process environment only.
82
+ `<env>` is `--env`, else `ZITADEL_ENV`, else a hosting platform's own signal
83
+ (`VERCEL_ENV`, or Netlify's `CONTEXT` inside a Netlify build), else
84
+ `development`. A missing value fails with `E_VALIDATION` naming the variable
85
+ and the files it looked in; there is no cloud default outside `setup`, and
86
+ the server is never read from `zitadel.json`. `zitadel env` shows what resolved and where
87
+ each value came from; `zitadel env add <name>` binds another environment to a
88
+ project. The project it creates has never had a release, so the environment
89
+ serves the server's own defaults until something is deployed to it: run the
90
+ `deploy` the result's `next_commands` names, against the file it wrote
91
+ (`--env-file .env.<name>.local`). A person at a terminal is asked whether to
92
+ deploy there and then; your runs never are, so they never deploy — run that
93
+ `deploy` yourself. `data.deployment` carries the `release_id`,
94
+ `content_hash`, `deployment_id` and `origins` when a person accepted, and is
95
+ null otherwise — a failed deploy is a warning, never a failed `env add`.
96
+ - **Origins.** `zitadel origin add <url> --kind primary|preview`, `origin list`
97
+ and `origin rm <url>` manage the URLs a project serves: a `primary` origin is
98
+ where the app is served, a `preview` pattern (it may hold a `*`) is where
99
+ pull-request builds run. They need the project secret, not the preview token.
100
+ An empty list admits every origin, so removing the last one opens the project
101
+ up; `origin rm` asks first and needs `--force` when non-interactive. Removing
102
+ a preview pattern leaves its live previews served until they expire.
103
+ `deploy` lands only on primary origins and fails with `E_VALIDATION` without
104
+ one; its `next_commands` name the step that gives the project one. `setup`
105
+ 0.1.0 registered the app's URL (`ZITADEL_ISSUER`) as a `preview` origin,
106
+ which `origin add --kind primary` refuses with `E_CONFLICT`: `doctor` warns
107
+ (`issuer-origin`) and `doctor --fix` makes it primary.
108
+ - **Telemetry.** The CLI sends anonymous usage telemetry by default, and
109
+ agent-driven runs are exactly the ones worth learning from — leave it on.
110
+ Don't add `--no-telemetry` to commands. An opt-out exists for users who need
111
+ it (`ZITADEL_TELEMETRY=0`, or the standard `DO_NOT_TRACK`, in their
112
+ environment), but that is the user's environment choice, not a flag to attach
113
+ to every call.
114
+ - **Flag conventions.** Flags follow what a model already expects from curl, ssh
115
+ and wget: `--help`/`-h`, `--version`, `-v`/`--verbose`, kebab-case long flags,
116
+ both `--flag value` and `--flag=value`, and `--no-telemetry`-style negation.
117
+ Two deliberate deviations worth knowing: `-n` is `--non-interactive` (not
118
+ `--dry-run`, which is long-only), and machine output is `--json` (not
119
+ `--output json`). `api` takes `-X` for the method, as curl does, and `-F` for
120
+ a typed field, as `gh api` and `vercel api` do (not curl's form field).
121
+ - **Writing a resource's body.** A `create` or `update` takes every field of
122
+ its body as a flag, nested ones too: a nested field's flag is its dot path
123
+ joined with `-` (`oidc.client_id` is `--oidc-client-id`), a list is a
124
+ repeated flag, a boolean takes `--no-<flag>` for `false`, and an open record
125
+ (`attributes`) takes repeated `key=value` entries. `--set <path>=<value>`
126
+ (or `<path>:=<json>` for JSON) reaches any field by its dot path, and
127
+ `--data` / `--file` take the whole document. They combine: the document
128
+ first, then the field flags, then each `--set`, with objects merging key by
129
+ key. `resources --json` lists each write's fields with their dot path
130
+ (`name`), `flag` (`null` when only `--set` reaches it), `kind` and
131
+ `required`; `--dry-run --json` returns the body that would be sent. For a
132
+ deeply nested body, writing the document and passing it with `--file -` is
133
+ often simplest.
134
+ - **Calling an operation no command covers.** `zitadel api <operation>` calls
135
+ any operation of the API with the resolved server and `.zitadel/secret`.
136
+ Name it by its `operationId` (`queryUsers`, as the API reference and
137
+ `@zitadel/api` spell it) or by its path (`/users/query`; `-X` picks the
138
+ method where a path has several). Path parameters follow as arguments, in
139
+ path order; a `project_id` left out is the project's own. `-F name=value`
140
+ (or `name:=json`) sets a query parameter when the operation declares one by
141
+ that name, else a dot path into the body, typed by the operation's schema;
142
+ a list parameter takes one `-F` per value. `--input <path>` or `--input -`
143
+ takes the whole body, under the `-F` fields. `--paginate` follows
144
+ `next_page_token` to the last page. `--dry-run` sends nothing and returns
145
+ the request. An operation only an end user's session or the login flow's
146
+ cookie authenticates is refused with `E_VALIDATION`. Prefer a resource
147
+ command where one exists: `api ls --json` names it in each operation's
148
+ `command`.
149
+ - **Never put secrets on the command line.** A property whose name reads as a
150
+ credential (`password`, `client_secret`, `api_key`, …) is refused anywhere on
151
+ argv when it holds free-form text — as a field flag, a `--set` or
152
+ `--attributes` entry, and inside an inline `--data` body alike, since argv is
153
+ visible to other processes and kept in shell history. Send such bodies with
154
+ `--file <path>` or `--file -` (stdin), which never pass through argv. A
155
+ `${{ NAME }}` reference to a project variable is not a secret and may stay on
156
+ argv (a connection's `client_secret` takes nothing else); write it in single
157
+ quotes, since inside double quotes the shell reads `${` as its own expansion.
158
+ - **Streams.** Capture stdout and stderr separately when scripting, into files
159
+ outside the app directory: `setup` scaffolds a new app only into an empty
160
+ directory. The machine contract is one parseable JSON object on stdout;
161
+ installer, audit, and package-manager progress belongs on stderr.
162
+
163
+ ## Discovering commands
164
+
165
+ The CLI ships its own command list and per-command flags, always correct for the
166
+ installed version. **Do not guess a command name.** Before running any command you
167
+ are not certain of, look it up first and use the exact name the CLI prints:
168
+ command and resource names — including whether a resource is singular or plural
169
+ and the exact verb — are whatever the CLI shows, never what you would assume. A
170
+ wrong guess costs a failed call and a round of recovery; one cheap lookup up
171
+ front avoids it. `zitadel --help` lists each family of commands (`users`,
172
+ `release`) as one row, so it stays short; `zitadel commands` is the flat list of
173
+ every command, verbs included, and `zitadel <family> --help` lists one family's
174
+ verbs. Treat the CLI's own output as authoritative over anything you remember:
175
+
176
+ ```sh
177
+ npx @zitadel/cli@latest --help # every command family, grouped by purpose
178
+ npx @zitadel/cli@latest commands # flat list of all commands
179
+ npx @zitadel/cli@latest <command> --help # a command's flags, which are required, and examples
180
+ npx @zitadel/cli@latest resources --json # the resource surface in one call (see below); contacts no server
181
+ npx @zitadel/cli@latest api ls --json # every API operation, its parameters, body and paging; contacts no server
182
+ ```
183
+
184
+ `--help` groups the commands by what you are doing: getting started, local
185
+ development, users and teams, authentication, configuration and releases, and
186
+ diagnostics and utilities. A family such as `users` is one row, and
187
+ `zitadel users --help` lists its verbs. Read the groups from `--help` rather than
188
+ from memory, since they change as the CLI gains commands.
189
+
190
+ Use `zitadel --help` to find *which* family or command to run, and
191
+ `zitadel commands` for the exact verb. Use `resources --json` when
192
+ you need a resource's *fields*: it reports the whole runtime-resource surface at
193
+ once — every resource, its verbs, each verb's `create_fields` / `update_fields`
194
+ (flag name, kind, `required`, any closed value set), its `filter_fields` /
195
+ `sort_fields`, and the `delete_outcome` a destructive verb returns — so prefer it
196
+ over reading `<resource> --help` one at a time. A `list` verb's envelope carries
197
+ `data.count`, the number of items it returned. A list returns one page unless
198
+ you pass `--all`, so a "how many …?" question is one `list --all --json` call:
199
+ read the count, do not page and tally.
200
+
201
+ ## Reading the envelope
202
+
203
+ Each invocation prints one JSON object:
204
+
205
+ - `status`: `ok` | `skipped` | `error`.
206
+ - `cli_version`, `command`, `source`: always present.
207
+ - On success: `data` with the command-specific payload.
208
+ - On a no-op: `reason` (e.g. `already-initialized`, `already-claimed`,
209
+ `nothing-to-eject`).
210
+ - On failure: `code` (e.g. `E_VALIDATION`, `E_NETWORK`, `E_NOT_FOUND`,
211
+ `E_CONFLICT`) and `message`, usually with a `hint`, and `details` when there
212
+ is structured context.
213
+ - `next_commands`: the suggested follow-ups, inside `data` on success and at
214
+ the top level on a no-op or failure. Prefer these over free-text hints.
215
+ - `warnings`: things that went wrong without stopping the command, such as a
216
+ value that was not published. Always present on success (often empty), and
217
+ present on a no-op or failure when there are any. Read them before you report
218
+ success: a warning usually means a follow-up step is needed.
219
+ - A `list` verb emits `data: { items, count, next_page_token }`; when a page
220
+ remains, `data.next_commands` carries the exact command for the next page.
221
+ - `deploy` emits `data.content_hash` (the release digest), `data.origins`
222
+ (the origins it went live on) and `data.resources` (how many it pinned). A
223
+ re-run on unchanged config returns the same digest. Use it to confirm what
224
+ went live.
225
+ - `preview` is `deploy` for a pull-request build: it deploys the release to the
226
+ build's preview URLs (from the platform, or `--origin`) for `--ttl` and emits
227
+ `data.previews` plus `data.release_env` (the `NEXT_PUBLIC_ZITADEL_RELEASE`
228
+ line to pin). It uses `ZITADEL_PROJECT_SECRET` until the server accepts the narrower preview token (#1575), never the other credential, and
229
+ outside a preview or development build it is a no-op — even with an explicit
230
+ `--origin`, which selects targets but never authorizes a preview deploy. A CI
231
+ build other than Vercel or Netlify must set `ZITADEL_ENV=preview`, or it is
232
+ skipped too. `preview list` and `preview rm <url>` read and end live previews.
233
+ - `deployment rollback` emits `data.release_id` (the release live again),
234
+ `data.origins` and `data.pointers`, the `{kind, handle, revision_id}`
235
+ revisions that release pins; the `schema` one is what new registrations use.
236
+ Users keep their schema revision and attributes: a rollback does not migrate
237
+ them back. Without `data.pointers`, a warning says the release could not be
238
+ read.
239
+ - A write verb's `--dry-run` reports what it would send without calling the
240
+ platform, so you can preview a mutation before making it.
241
+ - `api` emits the response body as `data`, exactly as the API reference
242
+ documents it (`null` for a response without one); with `--paginate`, the
243
+ page's list holds every page's items. `api --dry-run` emits the request
244
+ instead: `{operation, method, url, body?}`. A refusal from the server is the
245
+ error envelope, with its status and body in `details`.
246
+
247
+ Common error codes point at their own fix in `next_commands`:
248
+
249
+ - `E_VALIDATION`: the body or flags are wrong; `message` names what, and
250
+ `details` often lists the missing or available values.
251
+ - `E_CREDENTIAL_MISSING`: a connection's `client_id` or `client_secret` names a
252
+ project variable that does not exist; `details.variable` is the name. Publish it
253
+ with `variable set NAME` (add `--secret` for the secret), then retry.
254
+ - `E_LOCAL_SERVER_NOT_RUNNING`: start the local runtime, then retry with
255
+ `--server local`.
256
+ - `E_NOT_FOUND`: an HTTP 404 — a missing resource, an operation the server's
257
+ version does not have (`details.body.code` is `req.not_found`), or a
258
+ `--server` that is not a Zitadel platform API. Follow `next_commands`.
259
+ - `E_PORT_IN_USE`: the requested local runtime port already has a listener; stop
260
+ that process or choose another port.
261
+
262
+ In human mode the output follows the terminal (TTY table, or tab-separated lines
263
+ when piped or `--plain`); `--json` is unaffected and remains the contract.
264
+
265
+ Text the server returns is escaped before it is printed, in `--json` as in human
266
+ mode, so a stored value cannot drive the reader's terminal. Control, format and
267
+ bidi characters appear as visible `\xNN`, `\uNNNN` or `\u{NNNNN}` text, so a
268
+ value that contained one is not the stored value byte for byte — do not send it
269
+ back in an update as if it were. When you need the exact value, call the platform
270
+ API directly with the project secret.
271
+
272
+ Exit codes mirror the error class (3 = validation, 4 = network or not-found,
273
+ 5 = conflict, 1 = auth, 2 = not-implemented, 130 = cancelled). `E_NETWORK`
274
+ covers a server that answered 5xx and one that never answered — refused,
275
+ unresolvable, or silent past the 30-second request deadline. Ctrl-C exits
276
+ 130: a command waiting on the server, or on an answer at a prompt, stops at
277
+ once with `E_CANCELLED`, and any other command exits within a second, possibly
278
+ without an envelope. In human mode a cancel prints no error block. An
279
+ unknown command is handled by the CLI's help layer, not the envelope.
280
+
281
+ ## Clarify before you configure
282
+
283
+ Driving the CLI non-interactively does not mean deciding *for* the user. When
284
+ the user asks to add authentication but has not said *how*, don't silently pick
285
+ defaults — ask them first, in one short batched question, then run the CLI
286
+ non-interactively with their answers. The two things sit on different sides of
287
+ the conversation: you clarify with the human, then drive the CLI headlessly.
288
+
289
+ Ask about:
290
+
291
+ - **Sign-in methods** — password (the usual default), passkeys, and single
292
+ sign-on. If they want SSO, which providers? A provider the CLI does not list
293
+ by name is still possible: run `zitadel auth-method sso enable --help` to see
294
+ whether the installed CLI takes one, and which settings it needs, and ask for
295
+ those too.
296
+ - **Profile fields** — which attributes sign-up collects. The starting field
297
+ set depends on the use-case you pick (see *Presets and use-cases* below) —
298
+ the default (`minimal`) is the login identifier only, **not** names. Confirm
299
+ which fields they want (first/last name, phone, username, display name) and
300
+ choose the use-case or edit the schema accordingly.
301
+
302
+ Offer only what the installed CLI actually supports: read the real sign-in
303
+ presets, profile fields, and SSO options from the CLI first (`setup --help`,
304
+ `resources --json`) and present those, not a list you remember — the options
305
+ move with the CLI version.
306
+
307
+ ## Presets and use-cases — map the answers, don't trust the names
308
+
309
+ `setup` has two independent axes, and their names do **not** mean what they look
310
+ like. Read the current sets from `setup --help`, but know the traps:
311
+
312
+ - **Use-case = which profile fields the schema collects.** `minimal` is the
313
+ login identifier **only** — it does *not* include names. First + last name is
314
+ `--use-case consumer` (adds `givenName` + `familyName`); add a company with
315
+ `--use-case business`. Choosing `minimal` because the developer said "keep it
316
+ minimal" leaves an identifier-only schema that ignores the names they asked
317
+ for.
318
+ - **Preset = how they sign in (the login flow + auth methods).**
319
+ `password-first` makes password the primary step **but still leaves passkeys
320
+ enabled**; `passkey-first` leads with a passkey. Neither one gives you
321
+ password-only. If the developer does not want passkeys, turn them off
322
+ yourself: set `x-auth-methods.passkey.enabled` to `false` in
323
+ `.zitadel/schemas/*.json`, then `deploy`.
324
+
325
+ Translate the developer's answers into the right axis values, then **verify
326
+ against what actually deployed**: read the live schema back
327
+ (`schemas get <objectType> --json`) and confirm the fields and auth methods
328
+ match what they asked for — don't assume a preset name did it.
329
+
330
+ Skip the questions and proceed with the defaults above — stating the assumptions
331
+ you made so the user can correct them — in exactly two cases: the user already
332
+ told you what they want, or you are running non-interactively (a one-shot/headless
333
+ invocation) or were explicitly told to just set it up.
334
+
335
+ ## Answering the user
336
+
337
+ Report what changed, in the user's terms — not how you drove the CLI. The
338
+ flags, `--json`, the deploy flow, exit codes, and the number of
339
+ commands you ran are your internal business; the user asked for an outcome, not
340
+ a transcript. Say what they got.
341
+
342
+ - Good: "Added an optional phone number to your user schema and deployed it."
343
+ - Avoid: "I ran `deploy` with `--non-interactive --json` and it returned ok."
344
+
345
+ Quote a command or flag only when the user asks *how* something works, or when
346
+ they need to run something themselves. Otherwise keep the mechanics out of the
347
+ answer.
348
+
349
+ ## Golden path
350
+
351
+ The high-level workflow is stable even as individual flags evolve — run each
352
+ command's `--help` for its current flags (for example, `setup --help` lists the
353
+ framework, sign-in preset, and profile-field options):
354
+
355
+ ```sh
356
+ npx @zitadel/cli@latest doctor --non-interactive --json
357
+ npx @zitadel/cli@latest start --non-interactive --json
358
+ npx @zitadel/cli@latest setup --framework next --server local --non-interactive --json
359
+ npx @zitadel/cli@latest doctor --non-interactive --json
360
+ npx @zitadel/cli@latest deploy -m "<why this release changes>" --non-interactive --json
361
+ ```
362
+
363
+ `setup` scaffolds into an empty directory, or patches an existing app project
364
+ where it detects a framework. What `start` leaves in a directory —
365
+ `.zitadel/local/` and the `.gitignore` line that ignores it — still counts as
366
+ empty, so the golden path runs `setup` right after `start` in the same
367
+ directory. Any other non-empty directory stops setup with a hint to pass
368
+ `--force`. Pass it only for a directory whose contents may be replaced: files
369
+ the new app also has, such as `package.json`, are overwritten. Without a
370
+ terminal `setup` cannot ask which framework to scaffold, so pass `--framework`;
371
+ the error lists the choices in `details.frameworks`. `setup --dry-run` creates
372
+ no app: where it would scaffold one, it previews the project, and
373
+ `data.files_written` is empty, because those files depend on the generated app.
374
+ `setup` also writes and publishes the password policy
375
+ (`.zitadel/policies/auth.factor.password.set.json`) with a minimum length of
376
+ 15, NIST's minimum for password-only sign-in and the wizard's default. Pass
377
+ `--password-min-length <n>` only when the user asks for another value (`0` sets
378
+ none), and give test users passwords at least that long.
379
+ When setup fails after scaffolding a new app, before its default schema and
380
+ flow are uploaded, it removes the app again and puts back what the directory
381
+ held, so rerunning the same `setup` starts over.
382
+
383
+ `start` reuses a server already running in the directory only when it is the
384
+ release this CLI ships; one an earlier release started is restarted, and its
385
+ data stays. `setup --server local` refuses a binary-runtime server another
386
+ release started with `E_CONFLICT` before it creates anything: that server's API
387
+ is its own release's. Its `next_commands` run `start`, then `setup` again.
388
+
389
+ In an existing app, `--framework` has to name the framework setup detects
390
+ there; another one stops setup with `E_CONFLICT` instead of offering
391
+ `--force`. Setup reports its conflicts with the app before it creates a
392
+ project, and the error's `hint` says whether `--force` resolves one: it
393
+ replaces a file at a path setup writes, but not the app's root component or
394
+ request boundary at another path.
395
+
396
+ The app may also live in a subdirectory of the one `start` ran in: `setup`,
397
+ `console`, `logs`, `--server local` and `doctor` read `.zitadel/local/` from the working
398
+ directory or the nearest parent that has it (up to the home directory), so
399
+ `mkdir my-app && cd my-app` after `start` works too. `doctor` checks that
400
+ server and its port, but runs its project checks in the working directory, so run
401
+ it from the app. When that server is unhealthy it suggests `start` and
402
+ `reset --force` with `--cwd` set to the directory that holds it (when that path needs
403
+ shell quoting the commands are left out, the hint names the directory, and
404
+ `doctor --json` returns the argument lists in `details.suggested_args` to run as they are). A
405
+ local server is healthy once its `/readyz` answers, which takes its database
406
+ answering too: `start` waits for that, and the commands that read a server
407
+ treat one whose database does not answer as unhealthy. `stop`,
408
+ `reset` and `status` read only the working directory's
409
+ `.zitadel/local/`, so run them where `start` ran. On a local
410
+ server that hosts the platform project, `setup` attaches the project to the
411
+ local admin's team and ends `data.next_commands` with `console`; when it finds
412
+ no local admin for that server on the way up, it says so in the envelope's
413
+ `warnings` and the project stays unattached, so the local console does not
414
+ list it.
415
+
416
+ For a project set up against a hosted server, `doctor` checks no local
417
+ runtime, unless a local server was started in its directory or a parent, or
418
+ `--port`, `--runtime` or `--image` asks about one.
419
+
420
+ When `setup --sso` or `auth-method sso enable` adds a social sign-in provider, the user
421
+ must register the connection's redirect URI with the provider themselves: give
422
+ them `data.sso[].callback_uri` from `setup`, or `data.callback_uri` from
423
+ `auth-method sso enable`, verbatim. Each connection has its own URI, so a URI from another
424
+ provider or an older CLI version does not work.
425
+
426
+ After `setup`, follow `data.next_commands` to start the app. Prove the generated
427
+ auth flow in a visible browser by registering a unique user, logging out, logging
428
+ back in with the same email/password, and ending on the signed-in profile page.
429
+ Do not treat a rendered login or registration form as completion.
430
+
431
+ When a step against the local server fails without saying why (the login says a
432
+ sign-in provider "is not available right now", or a request returns 500), the
433
+ server logged the cause. Read it with `logs --level warn --json`: `data.logs`
434
+ holds the warnings and errors, and `--tail` counts only those lines. `logs
435
+ --follow` streams the log for a person at a terminal, with warnings yellow and
436
+ errors red; it refuses `--json`. `start`, and `doctor` when it finds the server
437
+ running, name it in `data.next_actions`.
438
+
439
+ To move a set-up app to another dev port (setup's `warnings` say the port is
440
+ already in use, the dev server exits with `EADDRINUSE`, or another app needs
441
+ the port), run `setup --dev-port <port>` in the app; never change the port in
442
+ one file by hand. In a directory with a
443
+ `zitadel.json`, `setup` returns `status: "skipped"` with `reason:
444
+ "already-initialized"`, unless `--dev-port` is given: then it moves the app.
445
+ It allows the new origin on the server first, and a refusal changes nothing.
446
+ It then rewrites the dev script or dev-server config, `ZITADEL_ISSUER` in
447
+ `.env.local`, `zitadel.json`, `.zitadel/secret` and the README/AGENTS.md
448
+ guidance, and removes the old origin. `data.previous_issuer` and `data.issuer`
449
+ name both origins, and `data.moved` is `false` when the app was already on that
450
+ port. A social sign-in provider still redirects to the old port: `warnings`
451
+ names the redirect URI to add at each provider. Restart the dev server
452
+ afterwards.
453
+
454
+ To change how users sign in after setup, use the `auth-method` commands. Each
455
+ sign-in method has its own command, with `--schema` when the project has more
456
+ than one:
457
+
458
+ - `auth-method password enable` and `auth-method password disable`
459
+ - `auth-method passkey enable` and `auth-method passkey disable`
460
+ - `auth-method sso enable --provider google` adds a provider. It prompts for
461
+ the client id and secret, or takes `--client-id` with the secret piped on
462
+ stdin when non-interactive. Any other OpenID Connect provider is
463
+ `--provider custom` with `--slug`, `--display-name`, `--issuer` and optionally
464
+ `--scopes` (default `openid profile email`).
465
+ In a terminal, a custom provider's settings you leave out are asked for, after
466
+ the checks that need no answer; a connection that already has that slug
467
+ supplies its own. A scripted, `--json`, `--dry-run` or `--non-interactive`
468
+ run must pass all three and gets an error naming the missing ones. After
469
+ writing a new connection the text output prints the claim mappings and names
470
+ the file to edit them in; `--json` does not carry them, so read the
471
+ connection file.
472
+ An issuer on `http://localhost:<port>` (a test provider on this machine)
473
+ works against a server `zitadel start` runs as a process: `start` sets
474
+ `NEXTGEN_HTTPCLIENT_ALLOW_LIST=localhost,127.0.0.0/8,::1/128` unless
475
+ `.env.local`, `.env` or the shell sets it, and setting it empty turns it off.
476
+ Under `--runtime docker` localhost is the container, so the provider is not
477
+ reachable. Otherwise `sso enable` warns in `warnings` that sign-in will fail.
478
+ `auth-method sso disable --provider google` removes a provider; a custom one
479
+ is named by its slug, as in `auth-method sso disable --provider acme-sso`.
480
+
481
+ The commands change only local files, apart from
482
+ `auth-method sso enable`, which also stores the provider's credentials on the
483
+ project. A provider added by `sso enable` is configured locally, not live: its
484
+ `data.title` says so, `data.next_actions` says to deploy, and `data.next_commands`
485
+ holds `deploy --dry-run` then `deploy`. Only run those once the provider's OAuth
486
+ application exists. `data.title` claims the credentials are stored on the project
487
+ only when they reached it. When they refuse, they write and publish nothing, on a dry run too.
488
+ They refuse to disable a method a login flow still asks for, to change a method
489
+ while a login flow has errors that prevent checking it, to enable password on a
490
+ schema without `x-identifier`, to edit a value that is not the shape they edit,
491
+ and to edit a schema that points at an external URL. Disabling the schema's last
492
+ way to sign in needs `--force` when non-interactive; only pass it for a schema
493
+ whose users are managed through the API, and ask the user first. That refusal's
494
+ `details.suggested_args` lists the `--force` re-run first, then the methods that
495
+ could be enabled instead. A dry run of the change is not refused: it previews it
496
+ and warns that no way to sign in would be left. A method that is enabled but not
497
+ offered by any active flow is reported in `warnings` (and in `data.not_offered`
498
+ for password and passkey). Every command reports `data.changed`, the
499
+ project-relative files it wrote (on a dry run, the files it would write), empty
500
+ when nothing changed. Follow `data.next_commands` (`deploy --dry-run`, `deploy`, and
501
+ `variable set` when a credential did not reach the project) to publish the
502
+ change. When a path or schema name would need shell quoting, `next_commands` is
503
+ empty and `data.next_args` (or `details.suggested_args` on a refusal) holds the
504
+ same commands as argument lists.
505
+
506
+ Repo config is authoritative: edit `zitadel.json` or files under `.zitadel/`,
507
+ then re-run `deploy`. See the reference below for driving the login UI
508
+ and the config-sync details. The editor schemas in `.zitadel/meta/` are
509
+ generated by the CLI, not config: never edit them. `doctor` warns when one is
510
+ missing or outdated (after an upgrade that changed a dialect), and
511
+ `doctor --fix` writes the current ones, unless the project pins a newer
512
+ `@zitadel/*` version (run that version's `doctor --fix`) or a symlink is on
513
+ the path; `deploy --dry-run` validates without them either way.
514
+
515
+ ## Claiming a project
516
+
517
+ A project on a hosted server stays temporary until a person claims it, within
518
+ 14 days of `setup`. Claiming signs that person in with their own Zitadel
519
+ account in a browser, so it is their step: hand them the link, and never open
520
+ it yourself. Plain `claim` waits for the browser step and prints nothing on
521
+ stdout until it ends, so run `claim --no-wait --non-interactive --json`
522
+ instead. It returns at once with `data.claim_url`, the link to give the person
523
+ verbatim, and `data.expires_at`, when it stops working (about 10 minutes).
524
+ Then run the `claim --challenge-id …` command in `data.next_commands`: it
525
+ waits for the person to finish, returns `data.dashboard_url`, and records the
526
+ owning team in `.zitadel/secret`. When the link expires, or `--challenge-id`
527
+ names one the platform does not know (`E_NOT_FOUND`), `next_commands` starts a
528
+ new `--no-wait` link. A project that already has a team is `skipped` with
529
+ `reason: "already-claimed"`; one past its 14 days fails with `E_VALIDATION`
530
+ and no `next_commands`, since a new link cannot reopen it.
531
+
532
+ ## Reference files
533
+
534
+ Load on demand:
535
+
536
+ - [`references/driving-login-ui.md`](references/driving-login-ui.md) — driving the
537
+ `<zitadel-login>` component in a browser, and the authoritative repo-config
538
+ workflow (schemas, flows, branding, policies).
@@ -0,0 +1,109 @@
1
+ ### Driving the login UI
2
+
3
+ `<zitadel-login>`, `<zitadel-session>`, and `<zitadel-logout>` are Lit elements
4
+ with open shadow roots. The stable automation hooks live inside nested shadow roots, so a flat
5
+ `document.querySelector('[data-testid="zitadel-input-email"]')` will not find
6
+ the native control. Browser drivers with shadow-DOM-aware locators, such as
7
+ Playwright, can target the hooks directly. Generic DOM-eval drivers should
8
+ pierce shadow roots recursively:
9
+
10
+ ```js
11
+ function deepQuery(sel, root = document) {
12
+ const hit = root.querySelector(sel);
13
+ if (hit) return hit;
14
+ for (const el of root.querySelectorAll("*")) {
15
+ if (el.shadowRoot) {
16
+ const result = deepQuery(sel, el.shadowRoot);
17
+ if (result) return result;
18
+ }
19
+ }
20
+ return null;
21
+ }
22
+ ```
23
+
24
+ Use host hooks such as `zitadel-field-email`, `zitadel-field-password`, and
25
+ `zitadel-action-submit` when targeting the Lit atoms. Use native shadow-control
26
+ hooks such as `zitadel-input-email`, `zitadel-input-password`, and
27
+ `zitadel-action-submit-button` when filling or clicking the underlying input or
28
+ button. Hooks stay method-named even when the flow engine names a credential
29
+ field `x-auth-methods#<method>`; only the `name` attribute carries that raw
30
+ form key. Enter inside a field submits the step's primary action, but only for
31
+ key events that carry `key: "Enter"` — drivers whose synthesized key events
32
+ omit it (some CDP wrappers) should click `zitadel-action-submit` instead.
33
+
34
+ For sign-out, the profile page that setup generates renders `<zitadel-session>`:
35
+ click its `zitadel-session-logout` host, or the native button inside it,
36
+ `zitadel-session-logout-button`. An app that renders `<zitadel-logout>` instead
37
+ hides sign-out behind an avatar menu: click the "Open user menu" button, then
38
+ pierce to `.signout-btn` (Playwright: `zitadel-logout .signout-btn`), which
39
+ carries no hook. A `<zitadel-logout>` with a `<template>` child shows no menu:
40
+ click the app's own element marked `data-action="logout"`. The canonical
41
+ component hook list lives in `packages/components/README.md`.
42
+
43
+ The checked-in automated regression path is `moon run e2e:journey`, which
44
+ exercises fresh-app setup plus registration, logout, and login across the
45
+ supported frameworks.
46
+
47
+ Repo config is authoritative: edit `zitadel.json` or files under `.zitadel/`,
48
+ then re-run `deploy` (or `deploy --dry-run` to validate and preview the diff
49
+ without publishing). Schema and flow files are synced from
50
+ `.zitadel/schemas/*.json` and `.zitadel/flows/*.json`. Login templates
51
+ (branding) are synced from `.zitadel/branding/`: a single `branding.json`
52
+ descriptor (layout, asset URLs) plus a sibling `login.liquid` LiquidJS
53
+ template referenced as `"liquid_template": { "$file": "./login.liquid" }`. Scaffold them with the
54
+ `branding eject` command (`--design centered|minimal`,
55
+ interactive picker on a TTY); setup never scaffolds them. Branding is
56
+ revisioned and immutable: every edit — including a `.liquid`-only edit —
57
+ is published as part of the next release when you `deploy`, and the login
58
+ serves the newest release. `deploy` validates templates with the authoritative
59
+ LiquidJS validator (and `deploy --dry-run` runs the same validation without
60
+ publishing; `E_VALIDATION` lists rule ids such as `no-script-tag` and
61
+ `mandatory-gates`; every template must keep a `{% mandatory_gates %}` tag,
62
+ where the login adds any required field and the primary action the template
63
+ left out). Asset URLs must be absolute `https://`; during development, images
64
+ may use `http://` on `localhost`, `127.x.x.x`, or `[::1]` (not `*.localhost`).
65
+ `typography.font_url` needs a `typography.font_family`, is always `https://`,
66
+ and is loaded only when the login runs as `variant="page"`, which setup
67
+ generates only when it creates a Next.js or Nuxt app itself, in an empty
68
+ directory. Keep exactly one descriptor in
69
+ `.zitadel/branding/` — extra `*.json` files there fail the scan.
70
+ Server-provisioned defaults remain a fallback for non-CLI project
71
+ creation, but CLI-created projects are authored from local files first. Flows are
72
+ revisioned like branding: an edit is published as a new immutable flow revision
73
+ in the next `deploy`. A flow names its schema by `objectType` in `user_schema`
74
+ and runs against the schema's newest revision, so a schema revise needs no
75
+ change to `user_schema`; one that removes or renames a property a step
76
+ collects needs that step's `fields` updated, and `deploy` checks each flow
77
+ against the schema it names. A login pinned by `flow-name` serves that flow's
78
+ newest active revision; an unpinned login serves the newest active unscoped
79
+ revision for its purpose in the project, whatever its flow's name. Older
80
+ revisions stay active, so an `audience` added after a flow was deployed
81
+ without one doesn't take that earlier revision out of the running. Removing a
82
+ flow file does not retire the
83
+ flow. Operation policies live in `.zitadel/policies/`: one `<operation>.json`
84
+ per guarded operation, named after its `operation`, holding
85
+ `{"$schema": "../meta/policy.json", "operation": "auth.factor.password.set",
86
+ "config": {"min_length": 15}}`. `deploy` checks each file against
87
+ `.zitadel/meta/policy.json` (known operation, only the template's settings,
88
+ within their bounds) and its name before anything is sent; settings that
89
+ contradict each other (a `min_length` above `max_length`) are refused by the
90
+ server's release build with `rel.invalid`, the policy's entry carrying
91
+ `pol.invalid`. A changed file publishes a new revision of the same policy,
92
+ which applies the next time a password is set. A setting left out is off, and
93
+ `"config": {}` switches every rule off. A sign-in or sign-up uses the policy
94
+ the deployed release carries, so deploying without the file switches it off
95
+ there; a password set through the API, or before the first deploy, uses the
96
+ newest revision, which only `"config": {}` switches off. `policies list` and
97
+ `policies get <id|operation>` read them back. `setup` writes and publishes
98
+ the password policy with a `min_length` of 15; pass `--password-min-length <n>`
99
+ only when the user asks for another value (`0` sets none). Managed files carry
100
+ a marker comment; `eject` removes only
101
+ files that still carry it, preserving anything the user replaced. For app-local
102
+ development, `--server local` resolves through `.zitadel/local/runtime.json` and
103
+ requires a healthy
104
+ `npx @zitadel/cli@latest start` runtime. The `.zitadel/local/` that `start`
105
+ writes into the directory still counts as empty, so a same-directory `setup`
106
+ scaffolds without `--force` (this is exactly what the golden path does).
107
+ `setup` installs dependencies
108
+ with the detected package manager by default; pass `--skip-install` when the
109
+ agent or host workflow will install dependencies separately.