@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/README.md +315 -131
- package/bin/run.js +8 -0
- package/dist/{groups-CMB0fMzE.mjs → groups-CbWm8kCm.mjs} +1 -1
- package/dist/{groups-CMB0fMzE.mjs.map → groups-CbWm8kCm.mjs.map} +1 -1
- package/dist/index.mjs +3001 -804
- package/dist/index.mjs.map +1 -1
- package/dist/lib/oclif/help.mjs +1 -1
- package/dist/lib/oclif/help.mjs.map +1 -1
- package/oclif.manifest.json +497 -3
- package/package.json +14 -10
- package/skills/zitadel-cli/SKILL.md +260 -0
- package/skills/zitadel-cli/references/driving-login-ui.md +72 -0
- package/SKILLS.md +0 -536
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.
|