@zitadel/cli 0.2.0 → 0.4.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.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Agent-friendly Zitadel CLI",
5
5
  "homepage": "https://github.com/zitadel/nextgen/tree/main/apps/cli#readme",
6
6
  "bugs": {
@@ -106,13 +106,16 @@
106
106
  "description": "View login flows."
107
107
  },
108
108
  "policies": {
109
- "description": "View policies."
109
+ "description": "View and test policies."
110
110
  },
111
111
  "env": {
112
112
  "description": "Show and bind environments."
113
113
  },
114
114
  "api": {
115
115
  "description": "Call any operation of the Zitadel API."
116
+ },
117
+ "skills": {
118
+ "description": "Give coding agents the Zitadel skills."
116
119
  }
117
120
  }
118
121
  },
@@ -127,9 +130,9 @@
127
130
  "@oclif/plugin-search": "^1.2.50",
128
131
  "@oclif/plugin-version": "^2.2.46",
129
132
  "@oclif/plugin-which": "^3.2.55",
130
- "@zitadel/api": "0.2.0",
131
- "@zitadel/config": "0.2.0",
132
- "@zitadel/server": "0.2.0",
133
+ "@zitadel/api": "0.4.0",
134
+ "@zitadel/config": "0.4.0",
135
+ "@zitadel/server": "0.4.0",
133
136
  "ajv": "^8.20.0",
134
137
  "consola": "^3.4.2",
135
138
  "magicast": "^0.5.3",
@@ -148,7 +151,8 @@
148
151
  "@types/wrap-ansi": "^3.0.0",
149
152
  "@vitest/coverage-v8": "^4.1.11",
150
153
  "@zitadel/api-mock": "0.0.0",
151
- "@zitadel/sdk-next": "0.2.0",
154
+ "@zitadel/sdk-next": "0.4.0",
155
+ "@zitadel/vitest-config": "0.0.0",
152
156
  "msw": "^2.14.6",
153
157
  "oclif": "^4.17.46",
154
158
  "tsdown": "^0.23.0",
@@ -47,6 +47,13 @@ by declared range or installed release, with `E_UNSUPPORTED_PROJECT_SHAPE`
47
47
  left the project untouched. The message names the minimum and the hint says to
48
48
  upgrade, so upgrade the app and rerun rather than retrying.
49
49
 
50
+ The CLI ships this skill and the other Zitadel skills. `skills add` puts the
51
+ ones of the version that runs it into the project's `.agents/skills/`, and into
52
+ `.claude/skills/` when Claude Code is installed or the project has a `.claude/`
53
+ folder, replacing older copies. After moving the app to a newer CLI, run it
54
+ with that version so the skills describe it. It talks to no server and asks
55
+ nothing; `data.skills` and `data.directories` say what it added where.
56
+
50
57
  ## Using the CLI
51
58
 
52
59
  - **Drive it non-interactively.** Always pass `--non-interactive --json` (short:
@@ -71,7 +78,10 @@ upgrade, so upgrade the app and rerun rather than retrying.
71
78
  `--help` output is bounded too — read it in full rather than `head`-ing it, or
72
79
  you will miss flags further down.
73
80
  - **Working directory.** Add `--cwd <path>` when operating outside the current
74
- working directory.
81
+ working directory. A relative `--env-file` path is still read from the
82
+ current directory, not from `--cwd` (`--cwd app --env-file app/.env.ci`):
83
+ Node reads `--env-file` itself before the CLI starts and exits 9 with
84
+ `node: <path>: not found`, and no envelope, when the file is not there.
75
85
  - **Server and project.** A command that talks to a project reads
76
86
  `ZITADEL_URL`, `ZITADEL_PROJECT_ID` and `ZITADEL_PROJECT_SECRET`, each from the
77
87
  first source that has it: `--server` (the URL only), `--env-file <path>`, the
@@ -93,6 +103,9 @@ upgrade, so upgrade the app and rerun rather than retrying.
93
103
  `deploy` yourself. `data.deployment` carries the `release_id`,
94
104
  `content_hash`, `deployment_id` and `origins` when a person accepted, and is
95
105
  null otherwise — a failed deploy is a warning, never a failed `env add`.
106
+ `ZITADEL_URL` and `--server` are the server's origin: the API lives under
107
+ `/api/v1` there, which the CLI adds (and drops from a URL that already ends
108
+ in it).
96
109
  - **Origins.** `zitadel origin add <url> --kind primary|preview`, `origin list`
97
110
  and `origin rm <url>` manage the URLs a project serves: a `primary` origin is
98
111
  where the app is served, a `preview` pattern (it may hold a `*`) is where
@@ -134,8 +147,9 @@ upgrade, so upgrade the app and rerun rather than retrying.
134
147
  - **Calling an operation no command covers.** `zitadel api <operation>` calls
135
148
  any operation of the API with the resolved server and `.zitadel/secret`.
136
149
  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
150
+ `@zitadel/api` spell it) or by its path as the spec has it (`/users/query`,
151
+ which the server serves at `/api/v1/users/query`; a path copied with the
152
+ prefix works too, and `-X` picks the method where a path has several). Path parameters follow as arguments, in
139
153
  path order; a `project_id` left out is the project's own. `-F name=value`
140
154
  (or `name:=json`) sets a query parameter when the operation declares one by
141
155
  that name, else a dot path into the body, typed by the operation's schema;
@@ -198,6 +212,11 @@ over reading `<resource> --help` one at a time. A `list` verb's envelope carries
198
212
  you pass `--all`, so a "how many …?" question is one `list --all --json` call:
199
213
  read the count, do not page and tally.
200
214
 
215
+ To find the users still on an older revision of a schema, filter on the
216
+ schema's object type and exclude the newest revision's URL:
217
+ `users list --filter schema_object_type=equals:<type> --filter
218
+ schema=not_equals:<newest-url> --all --json`.
219
+
201
220
  ## Reading the envelope
202
221
 
203
222
  Each invocation prints one JSON object:
@@ -230,12 +249,28 @@ Each invocation prints one JSON object:
230
249
  `--origin`, which selects targets but never authorizes a preview deploy. A CI
231
250
  build other than Vercel or Netlify must set `ZITADEL_ENV=preview`, or it is
232
251
  skipped too. `preview list` and `preview rm <url>` read and end live previews.
252
+ - `deploy` and `preview` record `-m` on the release and the deployment. Omitted,
253
+ they use a GitHub pull request's title, Vercel's commit message, or a clean
254
+ worktree's commit subject; with none, a non-interactive `deploy` fails. A
255
+ blank `-m` is refused. A release keeps the message of
256
+ the run that first built its content, usually the pull request's `preview`.
257
+ `deployment rollback -m` records why on the rollback.
258
+ - `deployment get <id>` emits the deployment as the API serves it; on a
259
+ terminal it also shows the variables the deployment froze, secrets masked.
260
+ Read them as JSON with `variable resolve`.
233
261
  - `deployment rollback` emits `data.release_id` (the release live again),
234
262
  `data.origins` and `data.pointers`, the `{kind, handle, revision_id}`
235
263
  revisions that release pins; the `schema` one is what new registrations use.
236
264
  Users keep their schema revision and attributes: a rollback does not migrate
237
265
  them back. Without `data.pointers`, a warning says the release could not be
238
266
  read.
267
+ - `deploy --dry-run` and `deploy` emit `data.diff`, the git diff the terminal
268
+ shows, against the live release's commit (`diff.against`): `diff.files` holds
269
+ one `{path, status, previous_path?, patch}` per changed file (`status` is
270
+ `added`, `modified`, `deleted`, `renamed` or `copied`; `patch` is its unified
271
+ diff), including rapid same-size edits and untracked configuration files.
272
+ Review it before you deploy. `files` is `[]` when nothing changed and
273
+ `null` when there is nothing to diff against, with `diff.note` saying why.
239
274
  - A write verb's `--dry-run` reports what it would send without calling the
240
275
  platform, so you can preview a mutation before making it.
241
276
  - `api` emits the response body as `data`, exactly as the API reference
@@ -254,13 +289,18 @@ Common error codes point at their own fix in `next_commands`:
254
289
  - `E_LOCAL_SERVER_NOT_RUNNING`: start the local runtime, then retry with
255
290
  `--server local`.
256
291
  - `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
292
+ version does not have (`details.body.code` is `req.not_found`; a server
293
+ from before the API moved under `/api/v1` answers every call so), or a
258
294
  `--server` that is not a Zitadel platform API. Follow `next_commands`.
259
295
  - `E_PORT_IN_USE`: the requested local runtime port already has a listener; stop
260
296
  that process or choose another port.
261
297
 
262
298
  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.
299
+ when piped or `--plain`); `--json` is unaffected and remains the contract. A
300
+ human run lists its next steps under `Next:` after its result (`setup` shows
301
+ them in its summary box instead); where stdout is data a script reads (a piped
302
+ or `--plain` list, a resource's created or updated JSON) they go to stderr, so
303
+ stdout holds only that data.
264
304
 
265
305
  Text the server returns is escaped before it is printed, in `--json` as in human
266
306
  mode, so a stored value cannot drive the reader's terminal. Control, format and
@@ -360,12 +400,50 @@ npx @zitadel/cli@latest doctor --non-interactive --json
360
400
  npx @zitadel/cli@latest deploy -m "<why this release changes>" --non-interactive --json
361
401
  ```
362
402
 
403
+ `setup` commits the project it scaffolds: `git init` first when the directory
404
+ is not yet a repository, then one commit, `chore: scaffold the project with
405
+ zitadel setup`. A release records the commit it was built from and `deploy`
406
+ diffs `.zitadel/` against it, so without this the first release carries no
407
+ commit and the next deploy reports no diff.
408
+
409
+ Then setup deploys `.zitadel/` to the app's origin, so the sign-in it asks you
410
+ to verify is served the release it built. `data.deployment` carries the
411
+ `release_id`, `content_hash`, `deployment_id` and `origins`, and is null on
412
+ `--dry-run` or when the deploy failed. A failed deploy is a warning, never a
413
+ failed setup, and `data.next_commands` then leads with the `deploy` that fixes
414
+ it.
415
+
416
+ Setup refuses with `E_CONFLICT` when the repository already has uncommitted
417
+ work, untracked files included, which `details.untracked` lists separately
418
+ from `details.modified`. The commit covers the project directory, so it can
419
+ only tell its own files from yours when the tree starts clean. It never
420
+ commits `.env*` files (other than `.env.example`), `.zitadel/secret` or
421
+ `.zitadel/local/`, whatever their case. Commit or stash
422
+ first, then rerun. Only what `start` leaves is exempt: `.zitadel/local/` and
423
+ the `.gitignore` line that ignores it. A connection under `.zitadel/idps/` is
424
+ yours and is not exempt. During a merge, rebase, cherry-pick, revert or bisect,
425
+ or on a detached HEAD, setup refuses with `E_CONFLICT` before it writes
426
+ anything, naming what stops the commit.
427
+
428
+ Setup deploys nothing, and it does not need to: a project with no release
429
+ resolves the newest revisions, which are the ones setup just uploaded, so
430
+ sign-in already runs on `.zitadel/`. Deploying pins them into a release, which
431
+ is what the server will require once that fallback goes (#1616). The publish
432
+ step says so and names the command. `data.git` reports what setup did:
433
+ `initialized` when it ran `git init`, even if the commit then failed, and the
434
+ `commit`, or null. `--no-git` skips both the refusal
435
+ and the commit, and nothing prompts, so this is the same interactively and in
436
+ CI.
437
+
363
438
  `setup` scaffolds into an empty directory, or patches an existing app project
364
439
  where it detects a framework. What `start` leaves in a directory —
365
440
  `.zitadel/local/` and the `.gitignore` line that ignores it — still counts as
366
441
  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
442
+ directory. A `--cwd` that does not exist yet counts as empty too: setup creates
443
+ it with the app, and removes it again when setup fails before the app is kept.
444
+ A `--cwd` that is a file fails with `E_VALIDATION`. Any other non-empty
445
+ directory stops setup with a hint to pass `--force`. Pass it only for a
446
+ directory whose contents may be replaced: files
369
447
  the new app also has, such as `package.json`, are overwritten. Without a
370
448
  terminal `setup` cannot ask which framework to scaffold, so pass `--framework`;
371
449
  the error lists the choices in `details.frameworks`. `setup --dry-run` creates
@@ -401,7 +479,11 @@ server and its port, but runs its project checks in the working directory, so ru
401
479
  it from the app. When that server is unhealthy it suggests `start` and
402
480
  `reset --force` with `--cwd` set to the directory that holds it (when that path needs
403
481
  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
482
+ `doctor --json` returns the argument lists in `details.suggested_args` to run as they are).
483
+ The hint says the server is the parent's and that `reset --force` deletes the data in that
484
+ directory, and it adds a plain `start`, which gives the working directory a server of its own
485
+ because `start` does not look in parents; a healthy parent server is reported in the `runtime`
486
+ check as started in the parent folder. A
405
487
  local server is healthy once its `/readyz` answers, which takes its database
406
488
  answering too: `start` waits for that, and the commands that read a server
407
489
  treat one whose database does not answer as unhealthy. `stop`,
@@ -511,6 +593,21 @@ missing or outdated (after an upgrade that changed a dialect), and
511
593
  `doctor --fix` writes the current ones, unless the project pins a newer
512
594
  `@zitadel/*` version (run that version's `doctor --fix`) or a symlink is on
513
595
  the path; `deploy --dry-run` validates without them either way.
596
+ `deploy`, `preview` and `release create`, `--dry-run` included, check the
597
+ shape of `.zitadel/` locally before anything is written, and fail with
598
+ `E_VALIDATION` listing each problem by field and by the schema's or flow's
599
+ handle (a policy by its file). The server still applies its own release rules
600
+ when the build is submitted, such as handles being unique, so a passing dry run
601
+ does not promise the release is accepted.
602
+
603
+ An SPA's dev proxy (`vite.config.*`, Angular's `proxy.conf.cjs`) keeps the
604
+ app's session in a cookie of its own project, `zitadel_session_<project id>`
605
+ on `http://localhost` and `__Host-zitadel_session_<project id>` over https.
606
+ `doctor` fails (`managed-files`, `outdated managed config wiring`) on the
607
+ session-cookie hooks a CLI wrote before these names: the server no longer
608
+ reads the cookies they keep. `doctor --fix` replaces hooks that are as the CLI
609
+ wrote them; ones changed by hand must be updated by hand, which the failure
610
+ says. Browsers signed in before the change sign in again.
514
611
 
515
612
  ## Claiming a project
516
613
 
@@ -535,4 +632,4 @@ Load on demand:
535
632
 
536
633
  - [`references/driving-login-ui.md`](references/driving-login-ui.md) — driving the
537
634
  `<zitadel-login>` component in a browser, and the authoritative repo-config
538
- workflow (schemas, flows, branding, policies).
635
+ workflow (schemas, flows, branding, policies and their tests).
@@ -94,7 +94,22 @@ which applies the next time a password is set. A setting left out is off, and
94
94
  the deployed release carries, so deploying without the file switches it off
95
95
  there; a password set through the API, or before the first deploy, uses the
96
96
  newest revision, which only `"config": {}` switches off. `policies list` and
97
- `policies get <id|operation>` read them back. `setup` writes and publishes
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
98
113
  the password policy with a `min_length` of 15; pass `--password-min-length <n>`
99
114
  only when the user asks for another value (`0` sets none). Managed files carry
100
115
  a marker comment; `eject` removes only