@zitadel/cli 0.1.0 → 0.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zitadel/cli",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Agent-friendly Zitadel CLI",
5
5
  "homepage": "https://github.com/zitadel/nextgen/tree/main/apps/cli#readme",
6
6
  "bugs": {
@@ -49,31 +49,31 @@
49
49
  ],
50
50
  "topics": {
51
51
  "users": {
52
- "description": "Manage the users of the project."
52
+ "description": "Create and manage users."
53
53
  },
54
54
  "teams": {
55
- "description": "Manage the teams of the project."
55
+ "description": "Create and manage teams."
56
56
  },
57
57
  "sessions": {
58
- "description": "Inspect and revoke the sessions of the project."
58
+ "description": "View and revoke sessions."
59
59
  },
60
60
  "events": {
61
- "description": "Read the audit event stream of the project."
61
+ "description": "View events."
62
62
  },
63
63
  "grants": {
64
- "description": "Manage collaboration grants on the project."
64
+ "description": "Manage access grants."
65
65
  },
66
66
  "projects": {
67
- "description": "Inspect and update the projects visible to this credential."
67
+ "description": "View and update projects."
68
68
  },
69
69
  "schemas": {
70
- "description": "Inspect user-schema revisions."
70
+ "description": "View user schemas."
71
71
  },
72
72
  "branding": {
73
- "description": "Work with login branding revisions."
73
+ "description": "Manage the login branding."
74
74
  },
75
75
  "auth-method": {
76
- "description": "Turn sign-in methods on or off for a user schema."
76
+ "description": "Configure how users sign in."
77
77
  },
78
78
  "auth-method:password": {
79
79
  "description": "Turn password sign-in on or off."
@@ -83,6 +83,36 @@
83
83
  },
84
84
  "auth-method:sso": {
85
85
  "description": "Add or remove identity providers."
86
+ },
87
+ "origin": {
88
+ "description": "Manage the project's allowed origins."
89
+ },
90
+ "release": {
91
+ "description": "Build and manage releases."
92
+ },
93
+ "deployment": {
94
+ "description": "Deploy releases and roll them back."
95
+ },
96
+ "preview": {
97
+ "description": "Deploy and manage pull-request preview URLs."
98
+ },
99
+ "variable": {
100
+ "description": "Manage project variables."
101
+ },
102
+ "idps": {
103
+ "description": "Manage identity provider connections."
104
+ },
105
+ "flow-definitions": {
106
+ "description": "View login flows."
107
+ },
108
+ "policies": {
109
+ "description": "View and test policies."
110
+ },
111
+ "env": {
112
+ "description": "Show and bind environments."
113
+ },
114
+ "api": {
115
+ "description": "Call any operation of the Zitadel API."
86
116
  }
87
117
  }
88
118
  },
@@ -97,20 +127,20 @@
97
127
  "@oclif/plugin-search": "^1.2.50",
98
128
  "@oclif/plugin-version": "^2.2.46",
99
129
  "@oclif/plugin-which": "^3.2.55",
100
- "@zitadel/api": "0.1.0",
101
- "@zitadel/config": "0.1.0",
102
- "@zitadel/server": "0.1.0",
130
+ "@zitadel/api": "0.3.0",
131
+ "@zitadel/config": "0.3.0",
132
+ "@zitadel/server": "0.3.0",
103
133
  "ajv": "^8.20.0",
104
134
  "consola": "^3.4.2",
105
135
  "magicast": "^0.5.3",
106
136
  "mixpanel": "^0.18.1",
137
+ "picocolors": "^1.1.1",
138
+ "rosie-skills": "^0.8.5",
107
139
  "safe-stable-stringify": "^2.5.0",
108
140
  "semver": "^7.7.3",
109
141
  "undici": "^7.29.1",
110
142
  "wrap-ansi": "^7.0.0",
111
- "zod": "^4.3.6",
112
- "picocolors": "^1.1.1",
113
- "rosie-skills": "^0.8.5"
143
+ "zod": "^4.3.6"
114
144
  },
115
145
  "devDependencies": {
116
146
  "@types/node": "^24.19.1",
@@ -118,13 +148,15 @@
118
148
  "@types/wrap-ansi": "^3.0.0",
119
149
  "@vitest/coverage-v8": "^4.1.11",
120
150
  "@zitadel/api-mock": "0.0.0",
121
- "@zitadel/sdk-next": "0.1.0",
151
+ "@zitadel/sdk-next": "0.3.0",
152
+ "@zitadel/vitest-config": "0.0.0",
122
153
  "msw": "^2.14.6",
123
154
  "oclif": "^4.17.46",
124
155
  "tsdown": "^0.23.0",
125
156
  "typescript": "^6.0.3",
126
157
  "vite": "^8.3.1",
127
- "vitest": "^4.1.11"
158
+ "vitest": "^4.1.11",
159
+ "yaml": "^2.9.0"
128
160
  },
129
161
  "scripts": {
130
162
  "build": "tsdown",
@@ -5,7 +5,7 @@ description: >-
5
5
  agent-friendly `zitadel` CLI. Use when the user wants to add login,
6
6
  registration, or session handling, create a Zitadel project, scaffold auth
7
7
  for a Next.js, Nuxt, SvelteKit, SolidStart, Qwik Router, TanStack Start, React,
8
- Vue, Angular, Solid, Svelte, or Qwik app, or plan and apply Zitadel config
8
+ Vue, Angular, Solid, Svelte, or Qwik app, or build and deploy Zitadel config
9
9
  changes from repo state.
10
10
  ---
11
11
 
@@ -67,11 +67,47 @@ upgrade, so upgrade the app and rerun rather than retrying.
67
67
  drops records or fields, so you end up reasoning from partial data. It is
68
68
  finite — read it whole, or narrow it *at the source*: use a `list` verb's
69
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.resources[].name'`).
70
+ just what you need (e.g. `… resources --json | jq '.data.items[].topic'`).
71
71
  `--help` output is bounded too — read it in full rather than `head`-ing it, or
72
72
  you will miss flags further down.
73
73
  - **Working directory.** Add `--cwd <path>` when operating outside the current
74
- working directory.
74
+ working directory. A relative `--env-file` path is still read from the
75
+ current directory, not from `--cwd` (`--cwd app --env-file app/.env.ci`):
76
+ Node reads `--env-file` itself before the CLI starts and exits 9 with
77
+ `node: <path>: not found`, and no envelope, when the file is not there.
78
+ - **Server and project.** A command that talks to a project reads
79
+ `ZITADEL_URL`, `ZITADEL_PROJECT_ID` and `ZITADEL_PROJECT_SECRET`, each from the
80
+ first source that has it: `--server` (the URL only), `--env-file <path>`, the
81
+ process environment, `.env.<env>.local`, `.env.local`, `.env.<env>`, `.env`,
82
+ then `.zitadel/secret`. Only the default `development` environment reads
83
+ `.env.local` and `.zitadel/secret` (setup's project); `--env-file` reads that
84
+ file and the process environment only.
85
+ `<env>` is `--env`, else `ZITADEL_ENV`, else a hosting platform's own signal
86
+ (`VERCEL_ENV`, or Netlify's `CONTEXT` inside a Netlify build), else
87
+ `development`. A missing value fails with `E_VALIDATION` naming the variable
88
+ and the files it looked in; there is no cloud default outside `setup`, and
89
+ the server is never read from `zitadel.json`. `zitadel env` shows what resolved and where
90
+ each value came from; `zitadel env add <name>` binds another environment to a
91
+ project. The project it creates has never had a release, so the environment
92
+ serves the server's own defaults until something is deployed to it: run the
93
+ `deploy` the result's `next_commands` names, against the file it wrote
94
+ (`--env-file .env.<name>.local`). A person at a terminal is asked whether to
95
+ deploy there and then; your runs never are, so they never deploy — run that
96
+ `deploy` yourself. `data.deployment` carries the `release_id`,
97
+ `content_hash`, `deployment_id` and `origins` when a person accepted, and is
98
+ null otherwise — a failed deploy is a warning, never a failed `env add`.
99
+ - **Origins.** `zitadel origin add <url> --kind primary|preview`, `origin list`
100
+ and `origin rm <url>` manage the URLs a project serves: a `primary` origin is
101
+ where the app is served, a `preview` pattern (it may hold a `*`) is where
102
+ pull-request builds run. They need the project secret, not the preview token.
103
+ An empty list admits every origin, so removing the last one opens the project
104
+ up; `origin rm` asks first and needs `--force` when non-interactive. Removing
105
+ a preview pattern leaves its live previews served until they expire.
106
+ `deploy` lands only on primary origins and fails with `E_VALIDATION` without
107
+ one; its `next_commands` name the step that gives the project one. `setup`
108
+ 0.1.0 registered the app's URL (`ZITADEL_ISSUER`) as a `preview` origin,
109
+ which `origin add --kind primary` refuses with `E_CONFLICT`: `doctor` warns
110
+ (`issuer-origin`) and `doctor --fix` makes it primary.
75
111
  - **Telemetry.** The CLI sends anonymous usage telemetry by default, and
76
112
  agent-driven runs are exactly the ones worth learning from — leave it on.
77
113
  Don't add `--no-telemetry` to commands. An opt-out exists for users who need
@@ -122,36 +158,40 @@ upgrade, so upgrade the app and rerun rather than retrying.
122
158
  `${{ NAME }}` reference to a project variable is not a secret and may stay on
123
159
  argv (a connection's `client_secret` takes nothing else); write it in single
124
160
  quotes, since inside double quotes the shell reads `${` as its own expansion.
125
- - **Streams.** Capture stdout and stderr separately when scripting. The machine
126
- contract is one parseable JSON object on stdout; installer, audit, and
127
- package-manager progress belongs on stderr.
161
+ - **Streams.** Capture stdout and stderr separately when scripting, into files
162
+ outside the app directory: `setup` scaffolds a new app only into an empty
163
+ directory. The machine contract is one parseable JSON object on stdout;
164
+ installer, audit, and package-manager progress belongs on stderr.
128
165
 
129
166
  ## Discovering commands
130
167
 
131
168
  The CLI ships its own command list and per-command flags, always correct for the
132
169
  installed version. **Do not guess a command name.** Before running any command you
133
- are not certain of, run `zitadel --help` first and use the exact name it prints:
170
+ are not certain of, look it up first and use the exact name the CLI prints:
134
171
  command and resource names — including whether a resource is singular or plural
135
- and the exact verb — are whatever `--help` shows, never what you would assume. A
136
- wrong guess costs a failed call and a round of recovery; one cheap `--help` up
137
- front avoids it. `zitadel --help` is a flat list and cheap to read, so reach for
138
- it first; treat the CLI's own output as authoritative over anything you remember:
172
+ and the exact verb — are whatever the CLI shows, never what you would assume. A
173
+ wrong guess costs a failed call and a round of recovery; one cheap lookup up
174
+ front avoids it. `zitadel --help` lists each family of commands (`users`,
175
+ `release`) as one row, so it stays short; `zitadel commands` is the flat list of
176
+ every command, verbs included, and `zitadel <family> --help` lists one family's
177
+ verbs. Treat the CLI's own output as authoritative over anything you remember:
139
178
 
140
179
  ```sh
141
- npx @zitadel/cli@latest --help # every command, grouped by purpose
180
+ npx @zitadel/cli@latest --help # every command family, grouped by purpose
142
181
  npx @zitadel/cli@latest commands # flat list of all commands
143
182
  npx @zitadel/cli@latest <command> --help # a command's flags, which are required, and examples
144
183
  npx @zitadel/cli@latest resources --json # the resource surface in one call (see below); contacts no server
145
184
  npx @zitadel/cli@latest api ls --json # every API operation, its parameters, body and paging; contacts no server
146
185
  ```
147
186
 
148
- `--help` groups the surface into project commands (create a project and scaffold
149
- auth), local-server commands (run a local Zitadel), configuration commands
150
- (`plan`/`apply` plus the config resources), and resource commands (the uniform
151
- `<resource> <verb>` surface). Read the groups from `--help` rather than from
152
- memory, since they change as the CLI gains commands.
187
+ `--help` groups the commands by what you are doing: getting started, local
188
+ development, users and teams, authentication, configuration and releases, and
189
+ diagnostics and utilities. A family such as `users` is one row, and
190
+ `zitadel users --help` lists its verbs. Read the groups from `--help` rather than
191
+ from memory, since they change as the CLI gains commands.
153
192
 
154
- Use `zitadel --help` to find *which* command to run. Use `resources --json` when
193
+ Use `zitadel --help` to find *which* family or command to run, and
194
+ `zitadel commands` for the exact verb. Use `resources --json` when
155
195
  you need a resource's *fields*: it reports the whole runtime-resource surface at
156
196
  once — every resource, its verbs, each verb's `create_fields` / `update_fields`
157
197
  (flag name, kind, `required`, any closed value set), its `filter_fields` /
@@ -161,6 +201,11 @@ over reading `<resource> --help` one at a time. A `list` verb's envelope carries
161
201
  you pass `--all`, so a "how many …?" question is one `list --all --json` call:
162
202
  read the count, do not page and tally.
163
203
 
204
+ To find the users still on an older revision of a schema, filter on the
205
+ schema's object type and exclude the newest revision's URL:
206
+ `users list --filter schema_object_type=equals:<type> --filter
207
+ schema=not_equals:<newest-url> --all --json`.
208
+
164
209
  ## Reading the envelope
165
210
 
166
211
  Each invocation prints one JSON object:
@@ -181,19 +226,30 @@ Each invocation prints one JSON object:
181
226
  success: a warning usually means a follow-up step is needed.
182
227
  - A `list` verb emits `data: { items, count, next_page_token }`; when a page
183
228
  remains, `data.next_commands` carries the exact command for the next page.
184
- - `plan` and `apply` emit `data.changes`: one row per touched resource
185
- (`{kind, action, file, id?, previous_id?, moved_from?}`, action ∈ create/
186
- update/revision/delete/move). A `move` is a file that took over a resource
187
- from a removed one (`moved_from`). Plan rows preview; apply rows report, with
188
- the resulting platform ids. Use it to verify an edit did what you intended.
189
- `data.unsupported_deletions` counts removed files whose resource cannot be
190
- deleted yet (they are kept, and are not part of `total`); on `apply` the files
191
- are listed in `data.unsupported_deletion_files`.
192
- - `apply` leaves the files under `.zitadel/` as written: what the server stores
193
- is never written back, and a field a file leaves out reads as the server's
194
- default, so `plan` reports no change after an `apply`. `data.files_updated`
195
- lists the one rewrite: a flow whose `user_schema` pinned a schema revision id
196
- the apply superseded, re-pinned to the new revision's id.
229
+ - `deploy` emits `data.content_hash` (the release digest), `data.origins`
230
+ (the origins it went live on) and `data.resources` (how many it pinned). A
231
+ re-run on unchanged config returns the same digest. Use it to confirm what
232
+ went live.
233
+ - `preview` is `deploy` for a pull-request build: it deploys the release to the
234
+ build's preview URLs (from the platform, or `--origin`) for `--ttl` and emits
235
+ `data.previews` plus `data.release_env` (the `NEXT_PUBLIC_ZITADEL_RELEASE`
236
+ line to pin). It uses `ZITADEL_PROJECT_SECRET` until the server accepts the narrower preview token (#1575), never the other credential, and
237
+ outside a preview or development build it is a no-op — even with an explicit
238
+ `--origin`, which selects targets but never authorizes a preview deploy. A CI
239
+ build other than Vercel or Netlify must set `ZITADEL_ENV=preview`, or it is
240
+ skipped too. `preview list` and `preview rm <url>` read and end live previews.
241
+ - `deployment rollback` emits `data.release_id` (the release live again),
242
+ `data.origins` and `data.pointers`, the `{kind, handle, revision_id}`
243
+ revisions that release pins; the `schema` one is what new registrations use.
244
+ Users keep their schema revision and attributes: a rollback does not migrate
245
+ them back. Without `data.pointers`, a warning says the release could not be
246
+ read.
247
+ - `deploy --dry-run` and `deploy` emit `data.diff`, the git diff the terminal
248
+ shows, against the live release's commit (`diff.against`): `diff.files` holds
249
+ one `{path, status, previous_path?, patch}` per changed file (`status` is
250
+ `added`, `modified`, `deleted`, `renamed` or `copied`; `patch` is its unified
251
+ diff). Review it before you deploy. `files` is `[]` when nothing changed and
252
+ `null` when there is nothing to diff against, with `diff.note` saying why.
197
253
  - A write verb's `--dry-run` reports what it would send without calling the
198
254
  platform, so you can preview a mutation before making it.
199
255
  - `api` emits the response body as `data`, exactly as the API reference
@@ -208,11 +264,12 @@ Common error codes point at their own fix in `next_commands`:
208
264
  `details` often lists the missing or available values.
209
265
  - `E_CREDENTIAL_MISSING`: a connection's `client_id` or `client_secret` names a
210
266
  project variable that does not exist; `details.variable` is the name. Publish it
211
- with `variables set NAME --project-level` (add `--secret` for the secret), then retry.
267
+ with `variable set NAME` (add `--secret` for the secret), then retry.
212
268
  - `E_LOCAL_SERVER_NOT_RUNNING`: start the local runtime, then retry with
213
269
  `--server local`.
214
- - `E_NOT_FOUND`: an HTTP 404 — a missing resource, or a `--server` that is not a
215
- Zitadel platform API. Follow `next_commands`.
270
+ - `E_NOT_FOUND`: an HTTP 404 — a missing resource, an operation the server's
271
+ version does not have (`details.body.code` is `req.not_found`), or a
272
+ `--server` that is not a Zitadel platform API. Follow `next_commands`.
216
273
  - `E_PORT_IN_USE`: the requested local runtime port already has a listener; stop
217
274
  that process or choose another port.
218
275
 
@@ -230,8 +287,9 @@ Exit codes mirror the error class (3 = validation, 4 = network or not-found,
230
287
  5 = conflict, 1 = auth, 2 = not-implemented, 130 = cancelled). `E_NETWORK`
231
288
  covers a server that answered 5xx and one that never answered — refused,
232
289
  unresolvable, or silent past the 30-second request deadline. Ctrl-C exits
233
- 130: a command waiting on the server stops at once with `E_CANCELLED`, and
234
- any other command exits within a second, possibly without an envelope. An
290
+ 130: a command waiting on the server, or on an answer at a prompt, stops at
291
+ once with `E_CANCELLED`, and any other command exits within a second, possibly
292
+ without an envelope. In human mode a cancel prints no error block. An
235
293
  unknown command is handled by the CLI's help layer, not the envelope.
236
294
 
237
295
  ## Clarify before you configure
@@ -276,7 +334,7 @@ like. Read the current sets from `setup --help`, but know the traps:
276
334
  enabled**; `passkey-first` leads with a passkey. Neither one gives you
277
335
  password-only. If the developer does not want passkeys, turn them off
278
336
  yourself: set `x-auth-methods.passkey.enabled` to `false` in
279
- `.zitadel/schemas/*.json`, then `apply`.
337
+ `.zitadel/schemas/*.json`, then `deploy`.
280
338
 
281
339
  Translate the developer's answers into the right axis values, then **verify
282
340
  against what actually deployed**: read the live schema back
@@ -291,13 +349,12 @@ invocation) or were explicitly told to just set it up.
291
349
  ## Answering the user
292
350
 
293
351
  Report what changed, in the user's terms — not how you drove the CLI. The
294
- flags, `--json`, the `plan`/`apply` split, exit codes, and the number of
352
+ flags, `--json`, the deploy flow, exit codes, and the number of
295
353
  commands you ran are your internal business; the user asked for an outcome, not
296
354
  a transcript. Say what they got.
297
355
 
298
356
  - Good: "Added an optional phone number to your user schema and deployed it."
299
- - Avoid: "I ran `plan` and `apply`, both with `--non-interactive --json`, and
300
- both returned ok."
357
+ - Avoid: "I ran `deploy` with `--non-interactive --json` and it returned ok."
301
358
 
302
359
  Quote a command or flag only when the user asks *how* something works, or when
303
360
  they need to run something themselves. Otherwise keep the mechanics out of the
@@ -314,21 +371,51 @@ npx @zitadel/cli@latest doctor --non-interactive --json
314
371
  npx @zitadel/cli@latest start --non-interactive --json
315
372
  npx @zitadel/cli@latest setup --framework next --server local --non-interactive --json
316
373
  npx @zitadel/cli@latest doctor --non-interactive --json
317
- npx @zitadel/cli@latest plan --non-interactive --json
318
- npx @zitadel/cli@latest apply --non-interactive --json
374
+ npx @zitadel/cli@latest deploy -m "<why this release changes>" --non-interactive --json
319
375
  ```
320
376
 
377
+ `setup` commits the project it scaffolds: `git init` first when the directory
378
+ is not yet a repository, then one commit, `chore: scaffold the project with
379
+ zitadel setup`. A release records the commit it was built from and `deploy`
380
+ diffs `.zitadel/` against it, so without this the first release carries no
381
+ commit and the next deploy reports no diff.
382
+
383
+ Setup refuses with `E_CONFLICT` when the repository already has uncommitted
384
+ work, untracked files included, which `details.untracked` lists separately
385
+ from `details.modified`. The commit covers the project directory, so it can
386
+ only tell its own files from yours when the tree starts clean. Commit or stash
387
+ first, then rerun. Only what `start` leaves is exempt: `.zitadel/local/` and
388
+ the `.gitignore` line that ignores it. A connection under `.zitadel/idps/` is
389
+ yours and is not exempt.
390
+
391
+ Setup deploys nothing, and it does not need to: a project with no release
392
+ resolves the newest revisions, which are the ones setup just uploaded, so
393
+ sign-in already runs on `.zitadel/`. Deploying pins them into a release, which
394
+ is what the server will require once that fallback goes (#1616). The publish
395
+ step says so and names the command. `data.git` reports what setup did
396
+ (`initialized`, and the `commit`, or null). `--no-git` skips both the refusal
397
+ and the commit, and nothing prompts, so this is the same interactively and in
398
+ CI.
399
+
321
400
  `setup` scaffolds into an empty directory, or patches an existing app project
322
401
  where it detects a framework. What `start` leaves in a directory —
323
402
  `.zitadel/local/` and the `.gitignore` line that ignores it — still counts as
324
403
  empty, so the golden path runs `setup` right after `start` in the same
325
- directory. Any other non-empty directory stops setup with a hint to pass
326
- `--force`. Pass it only for a directory whose contents may be replaced: files
404
+ directory. A `--cwd` that does not exist yet counts as empty too: setup creates
405
+ it with the app, and removes it again when setup fails before the app is kept.
406
+ A `--cwd` that is a file fails with `E_VALIDATION`. Any other non-empty
407
+ directory stops setup with a hint to pass `--force`. Pass it only for a
408
+ directory whose contents may be replaced: files
327
409
  the new app also has, such as `package.json`, are overwritten. Without a
328
410
  terminal `setup` cannot ask which framework to scaffold, so pass `--framework`;
329
411
  the error lists the choices in `details.frameworks`. `setup --dry-run` creates
330
412
  no app: where it would scaffold one, it previews the project, and
331
413
  `data.files_written` is empty, because those files depend on the generated app.
414
+ `setup` also writes and publishes the password policy
415
+ (`.zitadel/policies/auth.factor.password.set.json`) with a minimum length of
416
+ 15, NIST's minimum for password-only sign-in and the wizard's default. Pass
417
+ `--password-min-length <n>` only when the user asks for another value (`0` sets
418
+ none), and give test users passwords at least that long.
332
419
  When setup fails after scaffolding a new app, before its default schema and
333
420
  flow are uploaded, it removes the app again and puts back what the directory
334
421
  held, so rerunning the same `setup` starts over.
@@ -347,15 +434,22 @@ replaces a file at a path setup writes, but not the app's root component or
347
434
  request boundary at another path.
348
435
 
349
436
  The app may also live in a subdirectory of the one `start` ran in: `setup`,
350
- `console`, `--server local` and `doctor` read `.zitadel/local/` from the working
437
+ `console`, `logs`, `--server local` and `doctor` read `.zitadel/local/` from the working
351
438
  directory or the nearest parent that has it (up to the home directory), so
352
439
  `mkdir my-app && cd my-app` after `start` works too. `doctor` checks that
353
440
  server and its port, but runs its project checks in the working directory, so run
354
441
  it from the app. When that server is unhealthy it suggests `start` and
355
442
  `reset --force` with `--cwd` set to the directory that holds it (when that path needs
356
443
  shell quoting the commands are left out, the hint names the directory, and
357
- `doctor --json` returns the argument lists in `details.suggested_args` to run as they are). `stop`,
358
- `reset`, `logs` and `status` read only the working directory's
444
+ `doctor --json` returns the argument lists in `details.suggested_args` to run as they are).
445
+ The hint says the server is the parent's and that `reset --force` deletes the data in that
446
+ directory, and it adds a plain `start`, which gives the working directory a server of its own
447
+ because `start` does not look in parents; a healthy parent server is reported in the `runtime`
448
+ check as started in the parent folder. A
449
+ local server is healthy once its `/readyz` answers, which takes its database
450
+ answering too: `start` waits for that, and the commands that read a server
451
+ treat one whose database does not answer as unhealthy. `stop`,
452
+ `reset` and `status` read only the working directory's
359
453
  `.zitadel/local/`, so run them where `start` ran. On a local
360
454
  server that hosts the platform project, `setup` attaches the project to the
361
455
  local admin's team and ends `data.next_commands` with `console`; when it finds
@@ -363,10 +457,14 @@ no local admin for that server on the way up, it says so in the envelope's
363
457
  `warnings` and the project stays unattached, so the local console does not
364
458
  list it.
365
459
 
366
- When `setup --sso` or `sso enable` adds a social sign-in provider, the user
460
+ For a project set up against a hosted server, `doctor` checks no local
461
+ runtime, unless a local server was started in its directory or a parent, or
462
+ `--port`, `--runtime` or `--image` asks about one.
463
+
464
+ When `setup --sso` or `auth-method sso enable` adds a social sign-in provider, the user
367
465
  must register the connection's redirect URI with the provider themselves: give
368
466
  them `data.sso[].callback_uri` from `setup`, or `data.callback_uri` from
369
- `sso enable`, verbatim. Each connection has its own URI, so a URI from another
467
+ `auth-method sso enable`, verbatim. Each connection has its own URI, so a URI from another
370
468
  provider or an older CLI version does not work.
371
469
 
372
470
  After `setup`, follow `data.next_commands` to start the app. Prove the generated
@@ -374,9 +472,18 @@ auth flow in a visible browser by registering a unique user, logging out, loggin
374
472
  back in with the same email/password, and ending on the signed-in profile page.
375
473
  Do not treat a rendered login or registration form as completion.
376
474
 
377
- To move a set-up app to another dev port (the dev server exits with
378
- `EADDRINUSE`, or another app needs the port), run `setup --dev-port <port>` in
379
- the app; never change the port in one file by hand. In a directory with a
475
+ When a step against the local server fails without saying why (the login says a
476
+ sign-in provider "is not available right now", or a request returns 500), the
477
+ server logged the cause. Read it with `logs --level warn --json`: `data.logs`
478
+ holds the warnings and errors, and `--tail` counts only those lines. `logs
479
+ --follow` streams the log for a person at a terminal, with warnings yellow and
480
+ errors red; it refuses `--json`. `start`, and `doctor` when it finds the server
481
+ running, name it in `data.next_actions`.
482
+
483
+ To move a set-up app to another dev port (setup's `warnings` say the port is
484
+ already in use, the dev server exits with `EADDRINUSE`, or another app needs
485
+ the port), run `setup --dev-port <port>` in the app; never change the port in
486
+ one file by hand. In a directory with a
380
487
  `zitadel.json`, `setup` returns `status: "skipped"` with `reason:
381
488
  "already-initialized"`, unless `--dev-port` is given: then it moves the app.
382
489
  It allows the new origin on the server first, and a refusal changes nothing.
@@ -399,6 +506,13 @@ than one:
399
506
  stdin when non-interactive. Any other OpenID Connect provider is
400
507
  `--provider custom` with `--slug`, `--display-name`, `--issuer` and optionally
401
508
  `--scopes` (default `openid profile email`).
509
+ In a terminal, a custom provider's settings you leave out are asked for, after
510
+ the checks that need no answer; a connection that already has that slug
511
+ supplies its own. A scripted, `--json`, `--dry-run` or `--non-interactive`
512
+ run must pass all three and gets an error naming the missing ones. After
513
+ writing a new connection the text output prints the claim mappings and names
514
+ the file to edit them in; `--json` does not carry them, so read the
515
+ connection file.
402
516
  An issuer on `http://localhost:<port>` (a test provider on this machine)
403
517
  works against a server `zitadel start` runs as a process: `start` sets
404
518
  `NEXTGEN_HTTPCLIENT_ALLOW_LIST=localhost,127.0.0.0/8,::1/128` unless
@@ -410,7 +524,11 @@ than one:
410
524
 
411
525
  The commands change only local files, apart from
412
526
  `auth-method sso enable`, which also stores the provider's credentials on the
413
- project. When they refuse, they write and publish nothing, on a dry run too.
527
+ project. A provider added by `sso enable` is configured locally, not live: its
528
+ `data.title` says so, `data.next_actions` says to deploy, and `data.next_commands`
529
+ holds `deploy --dry-run` then `deploy`. Only run those once the provider's OAuth
530
+ application exists. `data.title` claims the credentials are stored on the project
531
+ only when they reached it. When they refuse, they write and publish nothing, on a dry run too.
414
532
  They refuse to disable a method a login flow still asks for, to change a method
415
533
  while a login flow has errors that prevent checking it, to enable password on a
416
534
  schema without `x-identifier`, to edit a value that is not the shape they edit,
@@ -423,15 +541,37 @@ and warns that no way to sign in would be left. A method that is enabled but not
423
541
  offered by any active flow is reported in `warnings` (and in `data.not_offered`
424
542
  for password and passkey). Every command reports `data.changed`, the
425
543
  project-relative files it wrote (on a dry run, the files it would write), empty
426
- when nothing changed. Follow `data.next_commands` (`plan`, `apply`, and
427
- `variables set` when a credential did not reach the project) to publish the
544
+ when nothing changed. Follow `data.next_commands` (`deploy --dry-run`, `deploy`, and
545
+ `variable set` when a credential did not reach the project) to publish the
428
546
  change. When a path or schema name would need shell quoting, `next_commands` is
429
547
  empty and `data.next_args` (or `details.suggested_args` on a refusal) holds the
430
548
  same commands as argument lists.
431
549
 
432
550
  Repo config is authoritative: edit `zitadel.json` or files under `.zitadel/`,
433
- then re-run `plan` and `apply`. See the reference below for driving the login UI
434
- and the config-sync details.
551
+ then re-run `deploy`. See the reference below for driving the login UI
552
+ and the config-sync details. The editor schemas in `.zitadel/meta/` are
553
+ generated by the CLI, not config: never edit them. `doctor` warns when one is
554
+ missing or outdated (after an upgrade that changed a dialect), and
555
+ `doctor --fix` writes the current ones, unless the project pins a newer
556
+ `@zitadel/*` version (run that version's `doctor --fix`) or a symlink is on
557
+ the path; `deploy --dry-run` validates without them either way.
558
+
559
+ ## Claiming a project
560
+
561
+ A project on a hosted server stays temporary until a person claims it, within
562
+ 14 days of `setup`. Claiming signs that person in with their own Zitadel
563
+ account in a browser, so it is their step: hand them the link, and never open
564
+ it yourself. Plain `claim` waits for the browser step and prints nothing on
565
+ stdout until it ends, so run `claim --no-wait --non-interactive --json`
566
+ instead. It returns at once with `data.claim_url`, the link to give the person
567
+ verbatim, and `data.expires_at`, when it stops working (about 10 minutes).
568
+ Then run the `claim --challenge-id …` command in `data.next_commands`: it
569
+ waits for the person to finish, returns `data.dashboard_url`, and records the
570
+ owning team in `.zitadel/secret`. When the link expires, or `--challenge-id`
571
+ names one the platform does not know (`E_NOT_FOUND`), `next_commands` starts a
572
+ new `--no-wait` link. A project that already has a team is `skipped` with
573
+ `reason: "already-claimed"`; one past its 14 days fails with `E_VALIDATION`
574
+ and no `next_commands`, since a new link cannot reopen it.
435
575
 
436
576
  ## Reference files
437
577
 
@@ -439,4 +579,4 @@ Load on demand:
439
579
 
440
580
  - [`references/driving-login-ui.md`](references/driving-login-ui.md) — driving the
441
581
  `<zitadel-login>` component in a browser, and the authoritative repo-config
442
- workflow (schemas, flows, branding).
582
+ workflow (schemas, flows, branding, policies and their tests).
@@ -45,7 +45,8 @@ exercises fresh-app setup plus registration, logout, and login across the
45
45
  supported frameworks.
46
46
 
47
47
  Repo config is authoritative: edit `zitadel.json` or files under `.zitadel/`,
48
- then re-run `plan` and `apply`. Schema and flow files are synced from
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
49
50
  `.zitadel/schemas/*.json` and `.zitadel/flows/*.json`. Login templates
50
51
  (branding) are synced from `.zitadel/branding/`: a single `branding.json`
51
52
  descriptor (layout, asset URLs) plus a sibling `login.liquid` LiquidJS
@@ -53,9 +54,10 @@ template referenced as `"liquid_template": { "$file": "./login.liquid" }`. Scaff
53
54
  `branding eject` command (`--design centered|minimal`,
54
55
  interactive picker on a TTY); setup never scaffolds them. Branding is
55
56
  revisioned and immutable: every edit — including a `.liquid`-only edit —
56
- plans as a `revise` and `apply` publishes a new revision; the login serves
57
- the newest one. `plan` validates templates with the authoritative LiquidJS
58
- validator (`E_VALIDATION` lists rule ids such as `no-script-tag` and
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
59
61
  `mandatory-gates`; every template must keep a `{% mandatory_gates %}` tag,
60
62
  where the login adds any required field and the primary action the template
61
63
  left out). Asset URLs must be absolute `https://`; during development, images
@@ -67,17 +69,50 @@ directory. Keep exactly one descriptor in
67
69
  `.zitadel/branding/` — extra `*.json` files there fail the scan.
68
70
  Server-provisioned defaults remain a fallback for non-CLI project
69
71
  creation, but CLI-created projects are authored from local files first. Flows are
70
- revisioned like branding: an edit plans as a `revise` and `apply` publishes a new
71
- immutable flow revision. A flow names its schema by `objectType` in
72
- `user_schema` and runs against the schema's newest revision, so a schema revise
73
- needs no change to `user_schema`; one that removes or renames a property a step
74
- collects needs that step's `fields` updated, and `plan` checks only the flows it
75
- uploads. A login pinned by `flow-name` serves that flow's newest active
76
- revision; an unpinned login serves the newest active unscoped revision for its
77
- purpose in the project, whatever its flow's name. Older revisions stay active,
78
- so an `audience` added after a flow was applied without one doesn't take that
79
- earlier revision out of the running. Removing a flow file does not retire the
80
- flow. Managed files carry a marker comment; `eject` removes only
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. A policy's cases live beside it
98
+ in `<operation>.test.json` (`.zitadel/meta/policy-test.json`): each names an
99
+ `input` in the operation's terms (`{"password": "…"}`), optional `facts` the
100
+ operation would look up (`self_service`, `history_matches`), and the expected
101
+ decision (`allowed`, optionally `code` and exactly the `violations`). The
102
+ decision and rules hold in the login too; `code` is the API's answer, and a
103
+ flow shows each rule as `error.x-auth-methods#password_<rule>` instead.
104
+ `policies test [operation]` evaluates every case on the server against the
105
+ sibling policy file, or with `--release <id>` against the revision that
106
+ release pins; a missing policy is tested as none. It fails with `E_VALIDATION`
107
+ (exit 3) when a case does not hold, `data`/`details` carrying `source`,
108
+ `operations[].cases[]` (`name`, `passed`, `expected`, `actual`), `passed` and
109
+ `failed`; with no test files it is `skipped` (`no-policy-tests`). `deploy`
110
+ never carries test files. Until #2331, a release that pins no flow is tested
111
+ with a warning: its sign-ins use the newest revisions instead. Write cases from
112
+ what the user wants enforced; never invent expectations for them. `setup` writes and publishes
113
+ the password policy with a `min_length` of 15; pass `--password-min-length <n>`
114
+ only when the user asks for another value (`0` sets none). Managed files carry
115
+ a marker comment; `eject` removes only
81
116
  files that still carry it, preserving anything the user replaced. For app-local
82
117
  development, `--server local` resolves through `.zitadel/local/runtime.json` and
83
118
  requires a healthy