@zitadel/cli 0.1.0-alpha.17 → 0.1.0-alpha.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +210 -124
  2. package/SKILLS.md +159 -17
  3. package/dist/branding-cygSBPkV.mjs +79 -0
  4. package/dist/branding-cygSBPkV.mjs.map +1 -0
  5. package/dist/claim-state-DlfD6Res.mjs +66 -0
  6. package/dist/claim-state-DlfD6Res.mjs.map +1 -0
  7. package/dist/commands/apply.mjs +6 -5
  8. package/dist/commands/apply.mjs.map +1 -1
  9. package/dist/commands/branding/eject.mjs +104 -0
  10. package/dist/commands/branding/eject.mjs.map +1 -0
  11. package/dist/commands/claim.mjs +402 -0
  12. package/dist/commands/claim.mjs.map +1 -0
  13. package/dist/commands/doctor.mjs +500 -37
  14. package/dist/commands/doctor.mjs.map +1 -1
  15. package/dist/commands/eject.mjs +4 -4
  16. package/dist/commands/eject.mjs.map +1 -1
  17. package/dist/commands/logs.mjs +2 -2
  18. package/dist/commands/plan.mjs +6 -5
  19. package/dist/commands/plan.mjs.map +1 -1
  20. package/dist/commands/reset.mjs +2 -2
  21. package/dist/commands/schemas/list.mjs +11 -11
  22. package/dist/commands/schemas/list.mjs.map +1 -1
  23. package/dist/commands/setup.mjs +359 -132
  24. package/dist/commands/setup.mjs.map +1 -1
  25. package/dist/commands/start.mjs +3 -3
  26. package/dist/commands/status.mjs +21 -13
  27. package/dist/commands/status.mjs.map +1 -1
  28. package/dist/commands/stop.mjs +3 -3
  29. package/dist/commands/stop.mjs.map +1 -1
  30. package/dist/designs-Ckz18Dpo.mjs +38 -0
  31. package/dist/designs-Ckz18Dpo.mjs.map +1 -0
  32. package/dist/{docker-pGcTrDIY.mjs → docker-DcRGTOJa.mjs} +4 -3
  33. package/dist/docker-DcRGTOJa.mjs.map +1 -0
  34. package/dist/{environment-BQF7LeCz.mjs → environment-rjRVkJjW.mjs} +1 -1
  35. package/dist/{environment-BQF7LeCz.mjs.map → environment-rjRVkJjW.mjs.map} +1 -1
  36. package/dist/{journey-guidance-CYVpQszz.mjs → journey-guidance-BGrOX_gT.mjs} +14 -3
  37. package/dist/{journey-guidance-CYVpQszz.mjs.map → journey-guidance-BGrOX_gT.mjs.map} +1 -1
  38. package/dist/{oclif-BRdjVNne.mjs → oclif-CanO3zdt.mjs} +247 -43
  39. package/dist/oclif-CanO3zdt.mjs.map +1 -0
  40. package/dist/{orca-Dip6DPJ2.mjs → orca-Cu0iW1mv.mjs} +894 -196
  41. package/dist/orca-Cu0iW1mv.mjs.map +1 -0
  42. package/dist/package-manager-DFdLcDx1.mjs +194 -0
  43. package/dist/package-manager-DFdLcDx1.mjs.map +1 -0
  44. package/dist/{ports-V8JfiNgT.mjs → ports-BM20XIZb.mjs} +2 -2
  45. package/dist/{ports-V8JfiNgT.mjs.map → ports-BM20XIZb.mjs.map} +1 -1
  46. package/dist/{processes-BVqYsxT8.mjs → processes-DKHPkU8O.mjs} +1 -1
  47. package/dist/{processes-BVqYsxT8.mjs.map → processes-DKHPkU8O.mjs.map} +1 -1
  48. package/dist/{project-BGgv7Bba.mjs → project-CHhot85s.mjs} +57 -4
  49. package/dist/project-CHhot85s.mjs.map +1 -0
  50. package/dist/{sync-Cd3z7Q1R.mjs → sync-Bd6vASgg.mjs} +549 -75
  51. package/dist/sync-Bd6vASgg.mjs.map +1 -0
  52. package/dist/user-schema-DTuOsdKE.mjs +91 -0
  53. package/dist/user-schema-DTuOsdKE.mjs.map +1 -0
  54. package/oclif.manifest.json +227 -5
  55. package/package.json +9 -6
  56. package/dist/docker-pGcTrDIY.mjs.map +0 -1
  57. package/dist/oclif-BRdjVNne.mjs.map +0 -1
  58. package/dist/orca-Dip6DPJ2.mjs.map +0 -1
  59. package/dist/project-BGgv7Bba.mjs.map +0 -1
  60. package/dist/sync-Cd3z7Q1R.mjs.map +0 -1
  61. package/dist/user-schema-DDz5-lX5.mjs +0 -13
  62. package/dist/user-schema-DDz5-lX5.mjs.map +0 -1
package/SKILLS.md CHANGED
@@ -51,6 +51,15 @@ Each invocation prints one JSON object:
51
51
  delete). Plan rows preview; apply rows report, with the resulting platform
52
52
  ids. Use it to verify an edit did what you intended — `apply`'s
53
53
  `files_updated` lists only local write-backs, not platform changes.
54
+ - `setup` emits `data.files`: one typed row per scaffolded artifact
55
+ (`{path, kind: file|dir, action: create|update}`), deduplicated. Use it to
56
+ see what setup created versus merged into (your `package.json` is an
57
+ `update`). `data.files_written` remains the flat list — deduplicated file
58
+ paths only, covering both scaffolded and `.zitadel/` resource files.
59
+ - `setup` also emits `data.design`: the starter login design it ejected and
60
+ published as branding revision 1, or `null` when the built-in template was
61
+ kept (no `.zitadel/branding/` files exist in that case). Use it to verify
62
+ the requested `--design` took effect without diffing the repo.
54
63
  - `E_LOCAL_SERVER_NOT_RUNNING`: start the local runtime with
55
64
  `npx @zitadel/cli@alpha start`, then retry with `--server local`.
56
65
  - `E_NOT_FOUND`: an HTTP 404 from the target server. With the platform's
@@ -80,33 +89,153 @@ the CLI's help layer, not the envelope.
80
89
  `.zitadel/flows/default-login.json`, uploads them through the schema and flow
81
90
  APIs, then seeds `.zitadel/state.json` so `plan` is immediately empty. Agents
82
91
  must pass `--framework` when scaffolding into a fresh directory; interactive
83
- humans can omit it and choose from the prompt. Flags:
92
+ humans can omit it and choose from the prompt. Supported floors: Next.js 15+
93
+ and React 18+ — `setup` and `doctor` fail with `E_UNSUPPORTED_PROJECT_SHAPE`
94
+ below them instead of degrading silently (an unparseable version passes).
95
+ Flags:
84
96
  `--framework next|react|vue|angular|nuxt|solid|svelte|qwik`, `--renderer
85
- react|web-component` (selects the Next.js auth-page renderer; accepted for any
86
- framework and recorded in `zitadel.json` branding, but only Next varies its
87
- generated templates by it), `--dev-port` (dev-server port, also the issuer
97
+ react` (selects the Next.js auth-page renderer; accepted for any framework
98
+ and recorded in `zitadel.json` branding, but only Next varies its generated
99
+ templates by it; the planned `web-component` renderer is not yet available
100
+ and is rejected if passed), `--dev-port` (dev-server port, also the issuer
88
101
  origin registered with Zitadel — use distinct ports to run several scaffolded
89
- apps side by side), `--preset password-first|passkey-first` (the sign-in
102
+ apps side by side. The app must actually serve this port or the flow API
103
+ rejects its origin on the first submit, so setup makes the port explicit in
104
+ the app's own dev-server config: `server.port` + `strictPort` for Vite
105
+ frameworks, `serve.options.port` for Angular, and — because `next dev` and
106
+ `nuxt dev` take a port only from the command line — `--port` in the
107
+ `package.json` `dev` script for Next and Nuxt. On a pre-existing app that
108
+ means setup edits the `dev` script when it does not already name that port;
109
+ a script already on it is left untouched), `--preset password-first|passkey-first` (the sign-in
90
110
  experience the scaffold starts from: `password-first` is the default —
91
111
  email + password with passkey optional during registration; `passkey-first`
92
112
  enters login on a one-tap passkey step with an email + password fallback;
93
- recorded in `zitadel.json`), `--skip-install`.
113
+ recorded in `zitadel.json`), `--use-case minimal|consumer|business` (which
114
+ profile fields the scaffolded schema collects: `minimal` is the default —
115
+ email only; `consumer` adds given and family name; `business` also adds a
116
+ `companyName` attribute and overlays work-email copy on the generated auth
117
+ pages via the SDK's `businessLocales`; asked before `--preset` and recorded
118
+ in `zitadel.json`), `--design centered|split|split-right|hero|minimal`
119
+ (starter login design: ejects the design's template into
120
+ `.zitadel/branding/` and publishes it as branding revision 1 during setup;
121
+ the interactive wizard asks this as its final question with the built-in
122
+ template preselected — omit the flag in non-interactive runs to keep the
123
+ built-in template and no branding files), `--skip-install`.
124
+ On Next and Nuxt, the scaffolded auth/profile pages derive their embedding
125
+ posture from the app: a fresh scaffold (setup created the skeleton) pins
126
+ `variant="page"` full-page chrome, while a pre-existing app embeds
127
+ `variant="widget"` cards with `theme="auto"` in a layout-neutral wrapper.
128
+ `theme="auto"` follows the OS `prefers-color-scheme`, not the host app's
129
+ own theme — edit the generated page to set `theme="light"` or
130
+ `theme="dark"` when the app pins its scheme. Other frameworks always
131
+ scaffold the page posture. The chosen posture is recorded in the scaffold
132
+ manifest, `doctor --fix` restores managed pages in the recorded posture,
133
+ and editing the generated page is the supported way to change presentation
134
+ — there is no config knob.
135
+ Widget-posture embedding levers: host-page CSS sets `--zl-*` design-token
136
+ custom properties on the element to bridge the app's look through the
137
+ widget's shadow DOM (fonts `--zl-font-family-heading`/`-sans`, radii
138
+ `--zl-radius-*`, primary CTA `--zl-primary`/`--zl-primary-foreground`,
139
+ link color `--zl-color-text-link`); the `suppress-header` attribute
140
+ (wrapper prop `suppressHeader`) visually hides the widget's own heading
141
+ block when the page already carries one, keeping it in the accessibility
142
+ tree. Split-family designs collapse their brand pane by container width —
143
+ at card width they show the compact brand mark (`logo_url`, else
144
+ `hero_url`, from `.zitadel/branding/branding.json`; `hero` falls back to
145
+ editable text), and setup warns when a widget-posture app picks `split`
146
+ or `split-right`.
94
147
  - `plan` — validate config and preview the sync diff without mutating anything.
95
148
  - `apply` — validate and upload repo config to the platform.
149
+ - `plan` and `apply --dry-run` also emit `data.warnings`: non-blocking
150
+ findings as `{path, rule, message}`, the same text the human plan prints as
151
+ `# warning:` lines and `apply` prints through stderr. They never fail a run.
152
+ Two families exist today: flow-definition rules (`warn/…`, mirrored from the
153
+ server's validator) and branding asset reachability. `warn/asset-unreachable`
154
+ and `warn/asset-content-type` come from a bounded HEAD probe of
155
+ `logo_url` / `hero_url` — a URL that is well-formed but dead passes every
156
+ gate and then renders as a 0×0 image with nothing in the console, so the
157
+ probe is the only place it can be caught. It is advisory by design: the
158
+ machine planning is not necessarily the machine that renders the login page.
159
+ The probe only contacts public HTTPS destinations and re-checks every
160
+ redirect; loopback/private/internal targets stay inconclusive instead of
161
+ turning repo config into a network request from the planning host.
162
+ Set `ZITADEL_SKIP_ASSET_PROBE` to turn it off (offline, air-gapped CI, or a
163
+ CDN that only resolves from production) and `ZITADEL_ASSET_PROBE_TIMEOUT_MS`
164
+ to retune the per-URL budget (default 2500).
165
+ - In the human-readable plan, a multi-line field (branding's inlined
166
+ `liquid_template`) renders as `(<n> lines, sha256:…)` when it is created or
167
+ unchanged, and as a changed-line diff when it moved — not as one escaped
168
+ line. Read the file itself for full content.
96
169
  - `schemas list` — inspect the revision history of a user-schema, filtered by
97
170
  `--object-type` (e.g. `human-user`). Non-interactive/`--json` prints one row
98
171
  per revision (newest first); interactive adds a picker that fetches and
99
172
  pretty-prints the selected revision body.
100
173
  - `doctor` — verify generated app files and local state once `zitadel.json`
101
- exists. The default local runtime is the `@zitadel/server` npm binary;
102
- Docker checks apply only when using `--runtime docker` or `--image`.
103
- `--fix` re-applies missing managed files.
174
+ exists. The `managed-files` check compares the scaffolded app files against
175
+ the manifest setup recorded in `.zitadel/state.json`: a missing
176
+ infrastructure file (the request boundary, `custom-elements.d.ts`) fails,
177
+ a missing generated page warns, and files you edited (marker kept) or
178
+ replaced (marker removed) pass as `edited`/`adopted`. It also verifies the
179
+ managed config wirings (Vite/Nuxt proxy merges, Angular's `angular.json`
180
+ proxy and auth routes) through the patchers' idempotent transforms — a
181
+ detached or missing wiring config fails, an unverifiable one warns, and
182
+ `--fix` re-applies it. The Next/Nuxt `dev` script is verified the same way,
183
+ against the port recorded as the development issuer rather than the port the
184
+ script names today: a script moved off that port reports as an unapplied
185
+ config edit (a warning — a `dev` script is not the only way to choose a
186
+ port), and `--fix` restores the registered one. Boundary migrations converge: a pristine leftover
187
+ `middleware.ts` from a Next 15→16 upgrade is swapped for `proxy.ts`, while
188
+ an edited one is reported as a conflict instead of creating both (Next
189
+ rejects the pair). The default local
190
+ runtime is the `@zitadel/server` npm binary; Docker checks apply only when
191
+ using `--runtime docker` or `--image`. `--fix` restores missing managed
192
+ files and never replaces an existing scaffolded app file; additive repairs
193
+ (missing `.gitignore` entries, `.env.example` keys) still append to their
194
+ targets, and the SDK dependency is re-added only when absent — an existing
195
+ version pin is never rewritten. The `dependency-version` check warns when
196
+ an exactly-pinned `@zitadel/*` dependency does not match the CLI's own
197
+ version (the packages release as one train, and a floating
198
+ `npx @zitadel/cli@alpha` can run ahead of the app's pins); ranges,
199
+ dist-tags, and `file:`/`workspace:` specifiers express a deliberate choice
200
+ and are not compared. The repair — an exact-pin install command for the
201
+ project's detected package manager — is emitted in `data.next_commands`
202
+ and quoted in the warning message.
203
+ - `claim` — attach the project to a team so it becomes permanent. Mints a
204
+ short-lived link, opens it in a browser, and blocks until the developer
205
+ finishes signing in there, then records `claimed_at` and `team_id` in
206
+ `.zitadel/secret`. Nothing about the project changes: the issuer, users,
207
+ passkeys, and applications keep working, and the project secret is not
208
+ rotated. Re-running once the project belongs to a team is a clean
209
+ `status: "skipped"` with `reason: "already-claimed"`, so agents can retry
210
+ safely. The link is always printed before any browser opens, so a headless
211
+ machine, an SSH session, or `--no-open` needs no special handling — copy it
212
+ and open it anywhere. Links last 10 minutes; once one lapses the command
213
+ exits `E_VALIDATION` and points at a fresh run. `--dry-run` stops before
214
+ anything is minted and reports `status: "skipped"`, `reason: "dry-run"` —
215
+ there is nothing to preview, because a claim is decided in a browser.
216
+ Flags: `--no-open` (print the link instead of launching a browser),
217
+ `--timeout <seconds>` (stop waiting sooner than the link's own expiry).
218
+ `setup`, `status`, and `doctor` report whether a team is attached, reading
219
+ `claimed_at`/`team_id` from `.zitadel/secret` (no platform call). `status`
220
+ carries `data.project.claim` as `{"kind": "detached"}` or
221
+ `{"kind": "attached", "team_id": "team_01H…", "claimed_at": "2026-08-01T09:00:00.000Z"}`,
222
+ and `doctor` reports a
223
+ `claim` check. A project with no team is only ever a **warning**, never a
224
+ failure — it works exactly like one with a team, so `doctor` still exits 0
225
+ and `--fix` deliberately does nothing (a claim needs a human in a browser).
226
+ All three stay silent about teams when the project's `server` in
227
+ `zitadel.json` is local or self-hosted, where there is nothing to attach.
104
228
  - `status` — summarize the local runtime and project state.
105
229
  - `eject` (alias `uninstall`) — remove managed files and local Zitadel state;
106
230
  requires `--force` when non-interactive.
107
231
  - `start` — start the managed local Zitadel server and persist runtime metadata
108
- under `.zitadel/local/runtime.json`. Use `--runtime docker` or `--image` for
109
- the Docker backend.
232
+ under `.zitadel/local/runtime.json`. The binary runtime defaults to SQLite
233
+ under `.zitadel/local/nextgen-data/`. Runtime metadata reports the published
234
+ server package version; the repository contributor wrapper reports the
235
+ `dev+<short-commit>` source build it launched. That label names the revision
236
+ the binary was built from, which after a Moon cache hit can be an earlier
237
+ commit whose server sources are byte-identical. Use `--runtime docker` or
238
+ `--image` for the Docker backend.
110
239
  - `stop` — stop the managed runtime while preserving
111
240
  `.zitadel/local/nextgen-data`. Use `stop --all` to sweep all discovered
112
241
  host-wide CLI-managed local runtime processes, including healthy runtimes
@@ -184,12 +313,25 @@ supported frameworks.
184
313
 
185
314
  Repo config is authoritative: edit `zitadel.json` or files under `.zitadel/`,
186
315
  then re-run `plan` and `apply`. Schema and flow files are synced from
187
- `.zitadel/schemas/*.json` and `.zitadel/flows/*.json`; templates are not
188
- supported until the server exposes template storage and APIs. Server-provisioned
189
- defaults remain a fallback for non-CLI project creation, but CLI-created
190
- projects are authored from local files first. Flow create, read, list, and
191
- update are available, while the server enforces lifecycle rules such as
192
- draft-only edits. Managed files carry a marker comment; `eject` removes only
316
+ `.zitadel/schemas/*.json` and `.zitadel/flows/*.json`. Login templates
317
+ (branding) are synced from `.zitadel/branding/`: a single `branding.json`
318
+ descriptor (layout, asset URLs) plus a sibling `login.liquid` LiquidJS
319
+ template referenced via `liquid_template_file`. Scaffold them with the
320
+ `branding eject` command (`--design centered|split|split-right|hero|minimal`,
321
+ interactive picker on a TTY) or at project creation with
322
+ `setup --design <name>`, which also publishes revision 1. Branding is
323
+ revisioned and immutable: every edit — including a `.liquid`-only edit —
324
+ plans as a `revise` and `apply` publishes a new revision; the login serves
325
+ the newest one. `plan` validates templates with the authoritative LiquidJS
326
+ validator (`E_VALIDATION` lists rule ids such as `no-script-tag` and
327
+ `mandatory-gates`; every template must keep a trailing
328
+ `{% mandatory_gates %}` tag). `font_url` is not writable yet; asset URLs
329
+ must be absolute `https://`. Keep exactly one descriptor in
330
+ `.zitadel/branding/` — extra `*.json` files there fail the scan.
331
+ Server-provisioned defaults remain a fallback for non-CLI project
332
+ creation, but CLI-created projects are authored from local files first. Flow create, read, list,
333
+ update, and delete are available, while the server enforces lifecycle rules
334
+ such as draft-only edits. Managed files carry a marker comment; `eject` removes only
193
335
  files that still carry it, preserving anything the user replaced. For app-local
194
336
  development, `--server local` resolves through `.zitadel/local/runtime.json` and
195
337
  requires a healthy
@@ -0,0 +1,79 @@
1
+ import { E as ZitadelError } from "./oclif-CanO3zdt.mjs";
2
+ import { isAbsolute, join, relative, resolve } from "node:path";
3
+ import { readFileSync } from "node:fs";
4
+ //#region src/lib/branding/index.ts
5
+ /**
6
+ * Relative directory (from the project root) where local branding files live:
7
+ * the `branding.json` descriptor plus the sibling `.liquid` template it
8
+ * references. Owned here so commands, syncers, and tests share one source of
9
+ * truth; only `*.json` files are sync-discovered, the template rides along
10
+ * via `liquid_template_file`.
11
+ */
12
+ const BRANDING_DIR = ".zitadel/branding";
13
+ /**
14
+ * Resolves a descriptor's `liquid_template_file` reference to an absolute
15
+ * path. References are relative to the descriptor's directory (all
16
+ * descriptors live flat in {@link BRANDING_DIR}) and must stay inside the
17
+ * project — a reference escaping `cwd` is an error because apply would later
18
+ * write server state back to that path.
19
+ */
20
+ function resolveTemplatePath(cwd, ref) {
21
+ const absolute = isAbsolute(ref) ? ref : resolve(join(cwd, BRANDING_DIR), ref);
22
+ const rel = relative(cwd, absolute);
23
+ if (rel.startsWith("..") || isAbsolute(rel)) throw new ZitadelError("E_VALIDATION", `liquid_template_file ${JSON.stringify(ref)} points outside the project`, { hint: `Keep the template next to its descriptor in ${BRANDING_DIR}/.` });
24
+ return absolute;
25
+ }
26
+ /**
27
+ * Returns the template string a descriptor carries — the inline
28
+ * `liquid_template`, or the content of `liquid_template_file`. Returns
29
+ * undefined when the descriptor has neither (a legal descriptor that only
30
+ * sets layout/asset URLs).
31
+ */
32
+ function readDescriptorTemplate(cwd, data) {
33
+ const descriptor = data;
34
+ if (typeof descriptor.liquid_template === "string") return descriptor.liquid_template;
35
+ if (typeof descriptor.liquid_template_file !== "string") return;
36
+ const path = resolveTemplatePath(cwd, descriptor.liquid_template_file);
37
+ try {
38
+ return readFileSync(path, "utf8");
39
+ } catch (error) {
40
+ throw new ZitadelError("E_VALIDATION", `liquid_template_file ${JSON.stringify(descriptor.liquid_template_file)} cannot be read`, {
41
+ hint: "Create the template file or fix the reference; the `branding eject` command scaffolds a starting point.",
42
+ details: { cause: error instanceof Error ? error.message : String(error) }
43
+ });
44
+ }
45
+ }
46
+ /**
47
+ * Converts a local descriptor to the wire body of `POST /branding`: strips
48
+ * the editor `$schema` affordance and replaces `liquid_template_file` with
49
+ * the inlined template content. Non-throwing on a missing template file (the
50
+ * field is simply left out) — `validate` reports that case with a hint
51
+ * before any planning happens.
52
+ */
53
+ function toBrandingWireBody(cwd, data) {
54
+ const { $schema, liquid_template_file, ...rest } = data;
55
+ const out = { ...rest };
56
+ if (typeof liquid_template_file === "string" && out.liquid_template === void 0) try {
57
+ out.liquid_template = readFileSync(resolveTemplatePath(cwd, liquid_template_file), "utf8");
58
+ } catch {}
59
+ return out;
60
+ }
61
+ /**
62
+ * Converts the server's canonical wire body back to the local descriptor
63
+ * form: when the local descriptor references a template file, the template
64
+ * string moves back out of the JSON into that reference. The caller writes
65
+ * the template content to the referenced file separately.
66
+ */
67
+ function toLocalBrandingBody(canonicalWire, localData) {
68
+ const local = localData;
69
+ if (typeof local.liquid_template_file !== "string") return canonicalWire;
70
+ const { liquid_template, ...rest } = canonicalWire;
71
+ return {
72
+ ...rest,
73
+ liquid_template_file: local.liquid_template_file
74
+ };
75
+ }
76
+ //#endregion
77
+ export { toLocalBrandingBody as a, toBrandingWireBody as i, readDescriptorTemplate as n, resolveTemplatePath as r, BRANDING_DIR as t };
78
+
79
+ //# sourceMappingURL=branding-cygSBPkV.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"branding-cygSBPkV.mjs","names":[],"sources":["../src/lib/branding/index.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { isAbsolute, join, relative, resolve } from \"node:path\";\n\nimport { ZitadelError } from \"../errors\";\n\n/**\n * Relative directory (from the project root) where local branding files live:\n * the `branding.json` descriptor plus the sibling `.liquid` template it\n * references. Owned here so commands, syncers, and tests share one source of\n * truth; only `*.json` files are sync-discovered, the template rides along\n * via `liquid_template_file`.\n */\nexport const BRANDING_DIR = \".zitadel/branding\";\n\ntype BrandingDescriptor = {\n liquid_template?: string;\n liquid_template_file?: string;\n [key: string]: unknown;\n};\n\n/**\n * Resolves a descriptor's `liquid_template_file` reference to an absolute\n * path. References are relative to the descriptor's directory (all\n * descriptors live flat in {@link BRANDING_DIR}) and must stay inside the\n * project — a reference escaping `cwd` is an error because apply would later\n * write server state back to that path.\n */\nexport function resolveTemplatePath(cwd: string, ref: string): string {\n const absolute = isAbsolute(ref) ? ref : resolve(join(cwd, BRANDING_DIR), ref);\n const rel = relative(cwd, absolute);\n if (rel.startsWith(\"..\") || isAbsolute(rel)) {\n throw new ZitadelError(\n \"E_VALIDATION\",\n `liquid_template_file ${JSON.stringify(ref)} points outside the project`,\n { hint: `Keep the template next to its descriptor in ${BRANDING_DIR}/.` },\n );\n }\n return absolute;\n}\n\n/**\n * Returns the template string a descriptor carries — the inline\n * `liquid_template`, or the content of `liquid_template_file`. Returns\n * undefined when the descriptor has neither (a legal descriptor that only\n * sets layout/asset URLs).\n */\nexport function readDescriptorTemplate(cwd: string, data: object): string | undefined {\n const descriptor = data as BrandingDescriptor;\n if (typeof descriptor.liquid_template === \"string\") {\n return descriptor.liquid_template;\n }\n if (typeof descriptor.liquid_template_file !== \"string\") {\n return undefined;\n }\n const path = resolveTemplatePath(cwd, descriptor.liquid_template_file);\n try {\n return readFileSync(path, \"utf8\");\n } catch (error) {\n throw new ZitadelError(\n \"E_VALIDATION\",\n `liquid_template_file ${JSON.stringify(descriptor.liquid_template_file)} cannot be read`,\n {\n hint: \"Create the template file or fix the reference; the `branding eject` command scaffolds a starting point.\",\n details: { cause: error instanceof Error ? error.message : String(error) },\n },\n );\n }\n}\n\n/**\n * Converts a local descriptor to the wire body of `POST /branding`: strips\n * the editor `$schema` affordance and replaces `liquid_template_file` with\n * the inlined template content. Non-throwing on a missing template file (the\n * field is simply left out) — `validate` reports that case with a hint\n * before any planning happens.\n */\nexport function toBrandingWireBody(cwd: string, data: object): object {\n const { $schema, liquid_template_file, ...rest } = data as BrandingDescriptor & {\n $schema?: string;\n };\n void $schema;\n const out: Record<string, unknown> = { ...rest };\n if (typeof liquid_template_file === \"string\" && out.liquid_template === undefined) {\n try {\n out.liquid_template = readFileSync(resolveTemplatePath(cwd, liquid_template_file), \"utf8\");\n } catch {\n // Reported by validate(); keep normalize/hashing total.\n }\n }\n return out;\n}\n\n/**\n * Converts the server's canonical wire body back to the local descriptor\n * form: when the local descriptor references a template file, the template\n * string moves back out of the JSON into that reference. The caller writes\n * the template content to the referenced file separately.\n */\nexport function toLocalBrandingBody(canonicalWire: object, localData: object): object {\n const local = localData as BrandingDescriptor;\n if (typeof local.liquid_template_file !== \"string\") {\n return canonicalWire;\n }\n const { liquid_template, ...rest } = canonicalWire as BrandingDescriptor;\n void liquid_template;\n return { ...rest, liquid_template_file: local.liquid_template_file };\n}\n"],"mappings":";;;;;;;;;;;AAYA,MAAa,eAAe;;;;;;;;AAe5B,SAAgB,oBAAoB,KAAa,KAAqB;CACpE,MAAM,WAAW,WAAW,IAAI,GAAG,MAAM,QAAQ,KAAK,KAAK,aAAa,EAAE,IAAI;CAC9E,MAAM,MAAM,SAAS,KAAK,SAAS;AACnC,KAAI,IAAI,WAAW,KAAK,IAAI,WAAW,IAAI,CACzC,OAAM,IAAI,aACR,gBACA,wBAAwB,KAAK,UAAU,IAAI,CAAC,8BAC5C,EAAE,MAAM,+CAA+C,aAAa,KAAK,CAC1E;AAEH,QAAO;;;;;;;;AAST,SAAgB,uBAAuB,KAAa,MAAkC;CACpF,MAAM,aAAa;AACnB,KAAI,OAAO,WAAW,oBAAoB,SACxC,QAAO,WAAW;AAEpB,KAAI,OAAO,WAAW,yBAAyB,SAC7C;CAEF,MAAM,OAAO,oBAAoB,KAAK,WAAW,qBAAqB;AACtE,KAAI;AACF,SAAO,aAAa,MAAM,OAAO;UAC1B,OAAO;AACd,QAAM,IAAI,aACR,gBACA,wBAAwB,KAAK,UAAU,WAAW,qBAAqB,CAAC,kBACxE;GACE,MAAM;GACN,SAAS,EAAE,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,EAAE;GAC3E,CACF;;;;;;;;;;AAWL,SAAgB,mBAAmB,KAAa,MAAsB;CACpE,MAAM,EAAE,SAAS,sBAAsB,GAAG,SAAS;CAInD,MAAM,MAA+B,EAAE,GAAG,MAAM;AAChD,KAAI,OAAO,yBAAyB,YAAY,IAAI,oBAAoB,KAAA,EACtE,KAAI;AACF,MAAI,kBAAkB,aAAa,oBAAoB,KAAK,qBAAqB,EAAE,OAAO;SACpF;AAIV,QAAO;;;;;;;;AAST,SAAgB,oBAAoB,eAAuB,WAA2B;CACpF,MAAM,QAAQ;AACd,KAAI,OAAO,MAAM,yBAAyB,SACxC,QAAO;CAET,MAAM,EAAE,iBAAiB,GAAG,SAAS;AAErC,QAAO;EAAE,GAAG;EAAM,sBAAsB,MAAM;EAAsB"}
@@ -0,0 +1,66 @@
1
+ import { M as serverKind, j as publicCliCommand } from "./oclif-CanO3zdt.mjs";
2
+ //#region src/lib/claim-state.ts
3
+ /**
4
+ * Whether the local record says a team owns this project.
5
+ *
6
+ * Both fields or neither: a half-written record is not an attachment, and
7
+ * treating it as one would hand callers a `team_id` with no idea when it
8
+ * landed. Shared with `zitadel claim`, which asks the same question before it
9
+ * decides to skip, so the two cannot drift as the record grows fields.
10
+ *
11
+ * Deliberately separate from {@link claimState}: the command must answer this
12
+ * on any server, while the nudges only ask it on the cloud.
13
+ */
14
+ function isAttached(secret) {
15
+ return Boolean(secret.claimed_at && secret.team_id);
16
+ }
17
+ /**
18
+ * Decides the attachment state from local state alone.
19
+ *
20
+ * **Why no network call.** The contract exposes no ambient claim-state read:
21
+ * `getClaimStatus` answers for one live challenge and rejects a secret that did
22
+ * not mint it, and claim attributes were deliberately kept off
23
+ * `GET /projects/{id}` because they belong to the grant, not the project. So
24
+ * `.zitadel/secret` is the only source, which is also why `status` and `doctor`
25
+ * stay fast and work offline. `zitadel claim` reaches the same conclusion the
26
+ * same way before it decides to skip.
27
+ *
28
+ * **Why cloud only.** Claiming attaches a project to a team on the platform.
29
+ * A local or self-hosted server has no such platform, and `--server local` is
30
+ * the documented dev loop, so an ungated nudge would advertise an impossible
31
+ * action for the entire life of a local project.
32
+ *
33
+ * The local record can be stale in one direction: a project claimed from
34
+ * another machine, or a `.zitadel/secret` restored from a backup, reads as
35
+ * `detached` here. That is tolerable because everything downstream is
36
+ * advisory, and `zitadel claim` itself turns the resulting `409` into a clean
37
+ * skip rather than an error.
38
+ */
39
+ function claimState(input) {
40
+ if (serverKind.value(input.server) !== "cloud") return { kind: "not-applicable" };
41
+ if (isAttached(input.secret)) return {
42
+ kind: "attached",
43
+ team_id: input.secret.team_id,
44
+ claimed_at: input.secret.claimed_at
45
+ };
46
+ return { kind: "detached" };
47
+ }
48
+ /**
49
+ * The nudge for `next_actions`, quoting the command the way `journey-guidance`
50
+ * does.
51
+ *
52
+ * Deliberately says nothing about deletion: unattached projects are framed as
53
+ * temporary, but the epic's 14-day lifetime is not enforced anywhere yet, so
54
+ * promising it would be a lie the product cannot keep.
55
+ */
56
+ function claimAction(cliVersion) {
57
+ return `This project is temporary until you attach it to a team: run ${publicCliCommand("claim", cliVersion)} to make it permanent. Nothing about the project changes, so users, passkeys, and the issuer keep working.`;
58
+ }
59
+ /** The command for `next_commands`. */
60
+ function claimCommand(cliVersion) {
61
+ return publicCliCommand("claim", cliVersion);
62
+ }
63
+ //#endregion
64
+ export { isAttached as i, claimCommand as n, claimState as r, claimAction as t };
65
+
66
+ //# sourceMappingURL=claim-state-DlfD6Res.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claim-state-DlfD6Res.mjs","names":[],"sources":["../src/lib/claim-state.ts"],"sourcesContent":["import { serverKind } from \"./oclif/server-kind\";\nimport type { ZitadelSecret } from \"./project\";\nimport { publicCliCommand } from \"./public-cli\";\n\n/**\n * Whether this project is attached to a team, as the CLI can tell locally.\n *\n * `not-applicable` is its own variant rather than a boolean flag on the caller\n * because \"there is nothing to attach here\" and \"nothing is attached yet\" want\n * opposite output: the first prints nothing at all, the second nudges.\n */\nexport type ClaimState =\n | { kind: \"attached\"; team_id: string; claimed_at: string }\n | { kind: \"detached\" }\n | { kind: \"not-applicable\" };\n\n/**\n * Whether the local record says a team owns this project.\n *\n * Both fields or neither: a half-written record is not an attachment, and\n * treating it as one would hand callers a `team_id` with no idea when it\n * landed. Shared with `zitadel claim`, which asks the same question before it\n * decides to skip, so the two cannot drift as the record grows fields.\n *\n * Deliberately separate from {@link claimState}: the command must answer this\n * on any server, while the nudges only ask it on the cloud.\n */\nexport function isAttached(\n secret: Pick<ZitadelSecret, \"claimed_at\" | \"team_id\">,\n): secret is { claimed_at: string; team_id: string } {\n return Boolean(secret.claimed_at && secret.team_id);\n}\n\n/**\n * Decides the attachment state from local state alone.\n *\n * **Why no network call.** The contract exposes no ambient claim-state read:\n * `getClaimStatus` answers for one live challenge and rejects a secret that did\n * not mint it, and claim attributes were deliberately kept off\n * `GET /projects/{id}` because they belong to the grant, not the project. So\n * `.zitadel/secret` is the only source, which is also why `status` and `doctor`\n * stay fast and work offline. `zitadel claim` reaches the same conclusion the\n * same way before it decides to skip.\n *\n * **Why cloud only.** Claiming attaches a project to a team on the platform.\n * A local or self-hosted server has no such platform, and `--server local` is\n * the documented dev loop, so an ungated nudge would advertise an impossible\n * action for the entire life of a local project.\n *\n * The local record can be stale in one direction: a project claimed from\n * another machine, or a `.zitadel/secret` restored from a backup, reads as\n * `detached` here. That is tolerable because everything downstream is\n * advisory, and `zitadel claim` itself turns the resulting `409` into a clean\n * skip rather than an error.\n */\nexport function claimState(input: {\n secret: Pick<ZitadelSecret, \"claimed_at\" | \"team_id\">;\n server: string;\n}): ClaimState {\n if (serverKind.value(input.server) !== \"cloud\") {\n return { kind: \"not-applicable\" };\n }\n if (isAttached(input.secret)) {\n return {\n kind: \"attached\",\n team_id: input.secret.team_id,\n claimed_at: input.secret.claimed_at,\n };\n }\n return { kind: \"detached\" };\n}\n\n/**\n * The nudge for `next_actions`, quoting the command the way `journey-guidance`\n * does.\n *\n * Deliberately says nothing about deletion: unattached projects are framed as\n * temporary, but the epic's 14-day lifetime is not enforced anywhere yet, so\n * promising it would be a lie the product cannot keep.\n */\nexport function claimAction(cliVersion: string): string {\n return (\n \"This project is temporary until you attach it to a team: run \" +\n `${publicCliCommand(\"claim\", cliVersion)} to make it permanent. ` +\n \"Nothing about the project changes, so users, passkeys, and the issuer keep working.\"\n );\n}\n\n/** The command for `next_commands`. */\nexport function claimCommand(cliVersion: string): string {\n return publicCliCommand(\"claim\", cliVersion);\n}\n"],"mappings":";;;;;;;;;;;;;AA2BA,SAAgB,WACd,QACmD;AACnD,QAAO,QAAQ,OAAO,cAAc,OAAO,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;AAyBrD,SAAgB,WAAW,OAGZ;AACb,KAAI,WAAW,MAAM,MAAM,OAAO,KAAK,QACrC,QAAO,EAAE,MAAM,kBAAkB;AAEnC,KAAI,WAAW,MAAM,OAAO,CAC1B,QAAO;EACL,MAAM;EACN,SAAS,MAAM,OAAO;EACtB,YAAY,MAAM,OAAO;EAC1B;AAEH,QAAO,EAAE,MAAM,YAAY;;;;;;;;;;AAW7B,SAAgB,YAAY,YAA4B;AACtD,QACE,gEACG,iBAAiB,SAAS,WAAW,CAAC;;;AAM7C,SAAgB,aAAa,YAA4B;AACvD,QAAO,iBAAiB,SAAS,WAAW"}
@@ -1,7 +1,7 @@
1
- import { t as BaseCommand, x as publicCliCommand } from "../oclif-BRdjVNne.mjs";
2
- import { o as readZitadelSecret } from "../project-BGgv7Bba.mjs";
3
- import { a as buildSyncPlan, i as summarizePlan, n as enumeratePlanResources, r as renderPlan, s as runSyncLoop, t as collectPlanWarnings, u as makeSyncers } from "../sync-Cd3z7Q1R.mjs";
4
- import { t as environmentSchema } from "../environment-BQF7LeCz.mjs";
1
+ import { j as publicCliCommand, t as BaseCommand } from "../oclif-CanO3zdt.mjs";
2
+ import { l as readZitadelSecret } from "../project-CHhot85s.mjs";
3
+ import { a as buildSyncPlan, i as summarizePlan, l as makeSyncers, n as enumeratePlanResources, r as renderPlan, s as runSyncLoop, t as collectPlanWarnings } from "../sync-Bd6vASgg.mjs";
4
+ import { t as environmentSchema } from "../environment-rjRVkJjW.mjs";
5
5
  import { Flags } from "@oclif/core";
6
6
  import { createZitadelClient } from "@zitadel/api/client";
7
7
  import { consola as consola$1 } from "consola";
@@ -35,7 +35,8 @@ var Apply = class Apply extends BaseCommand {
35
35
  token: secret.project_secret
36
36
  }),
37
37
  projectId: secret.project_id,
38
- env
38
+ env,
39
+ cwd
39
40
  });
40
41
  if (!dryRun) {
41
42
  consola$1.start("Syncing schemas and flows to Zitadel");
@@ -1 +1 @@
1
- {"version":3,"file":"apply.mjs","names":[],"sources":["../../src/commands/apply.ts"],"sourcesContent":["import { Flags } from \"@oclif/core\";\nimport { consola } from \"consola\";\n\nimport { createZitadelClient } from \"@zitadel/api/client\";\n\nimport { BaseCommand, type JsonEnvelope } from \"../lib/oclif\";\nimport { environmentSchema } from \"../lib/environment\";\nimport {\n buildSyncPlan,\n collectPlanWarnings,\n enumeratePlanResources,\n makeSyncers,\n renderPlan,\n runSyncLoop,\n summarizePlan,\n} from \"../lib/sync\";\nimport { readZitadelSecret } from \"../lib/project\";\nimport { publicCliCommand } from \"../lib/public-cli\";\n\n/**\n * `zitadel apply` — validate and upload repo config to the platform.\n *\n * Runs the sync loop to convergence, or (with `--dry-run`) previews the diff\n * without mutating. All validation — structural shape and `${VAR}` / `*_env`\n * reference presence — happens inside the sync engine ({@link buildSyncPlan}),\n * so an invalid or under-configured file fails with `E_VALIDATION` before any\n * platform call.\n */\nexport default class Apply extends BaseCommand {\n static override description = \"Validate and upload repo config to the platform.\";\n static override flags = {\n environment: Flags.string({\n char: \"e\",\n description: \"Target environment (default: development).\",\n options: [...environmentSchema.options],\n }),\n };\n\n async run(): Promise<JsonEnvelope> {\n const { flags } = await this.parse(Apply);\n await this.toMeta(flags);\n const { cwd, source, env, dryRun, isTTY } = this.meta;\n\n const secret = await readZitadelSecret(cwd);\n consola.info(`Project ${secret.project_id}`);\n consola.info(`Server ${source}`);\n const client = createZitadelClient({\n baseUrl: source,\n token: secret.project_secret,\n });\n const syncers = makeSyncers({\n client,\n projectId: secret.project_id,\n env,\n });\n\n if (!dryRun) {\n consola.start(\"Syncing schemas and flows to Zitadel\");\n const { filesUpdated, applied } = await runSyncLoop(cwd, syncers);\n consola.success(\"Sync complete\");\n return this.emit({\n status: \"ok\",\n data: {\n synced: true,\n // Platform resources this run touched (with resulting ids);\n // `files_updated` stays the local write-backs only.\n changes: applied,\n files_updated: filesUpdated,\n next_actions:\n applied.length === 0\n ? [\"Everything is already in sync — no changes were applied.\"]\n : [\n \"Changes are live — reload your app to see them.\",\n \"Re-run plan to confirm local config and platform are in sync.\",\n ],\n next_commands: [publicCliCommand(\"plan\", this.meta.cliVersion)],\n },\n });\n }\n\n consola.start(\"Building plan (dry run)\");\n const plan = await buildSyncPlan(cwd, syncers, true);\n const summary = summarizePlan(plan);\n this.recordTelemetry({\n creates: summary.creates,\n updates: summary.updates,\n revisions: summary.revisions,\n deletes: summary.deletes,\n total: summary.total,\n });\n consola.success(\n `Plan: ${summary.creates} create${summary.creates === 1 ? \"\" : \"s\"}, ` +\n `${summary.updates} update${summary.updates === 1 ? \"\" : \"s\"}, ` +\n `${summary.revisions} new revision${summary.revisions === 1 ? \"\" : \"s\"}, ` +\n `${summary.deletes} delete${summary.deletes === 1 ? \"\" : \"s\"}`,\n );\n return this.emit({\n status: \"ok\",\n data: {\n ...summary,\n changes: enumeratePlanResources(plan),\n warnings: collectPlanWarnings(plan),\n },\n pretty: renderPlan(plan, isTTY),\n });\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA4BA,IAAqB,QAArB,MAAqB,cAAc,YAAY;CAC7C,OAAgB,cAAc;CAC9B,OAAgB,QAAQ,EACtB,aAAa,MAAM,OAAO;EACxB,MAAM;EACN,aAAa;EACb,SAAS,CAAC,GAAG,kBAAkB,QAAQ;EACxC,CAAC,EACH;CAED,MAAM,MAA6B;EACjC,MAAM,EAAE,UAAU,MAAM,KAAK,MAAM,MAAM;AACzC,QAAM,KAAK,OAAO,MAAM;EACxB,MAAM,EAAE,KAAK,QAAQ,KAAK,QAAQ,UAAU,KAAK;EAEjD,MAAM,SAAS,MAAM,kBAAkB,IAAI;AAC3C,YAAQ,KAAK,aAAa,OAAO,aAAa;AAC9C,YAAQ,KAAK,aAAa,SAAS;EAKnC,MAAM,UAAU,YAAY;GAC1B,QALa,oBAAoB;IACjC,SAAS;IACT,OAAO,OAAO;IACf,CAEO;GACN,WAAW,OAAO;GAClB;GACD,CAAC;AAEF,MAAI,CAAC,QAAQ;AACX,aAAQ,MAAM,uCAAuC;GACrD,MAAM,EAAE,cAAc,YAAY,MAAM,YAAY,KAAK,QAAQ;AACjE,aAAQ,QAAQ,gBAAgB;AAChC,UAAO,KAAK,KAAK;IACf,QAAQ;IACR,MAAM;KACJ,QAAQ;KAGR,SAAS;KACT,eAAe;KACf,cACE,QAAQ,WAAW,IACf,CAAC,2DAA2D,GAC5D,CACE,mDACA,gEACD;KACP,eAAe,CAAC,iBAAiB,QAAQ,KAAK,KAAK,WAAW,CAAC;KAChE;IACF,CAAC;;AAGJ,YAAQ,MAAM,0BAA0B;EACxC,MAAM,OAAO,MAAM,cAAc,KAAK,SAAS,KAAK;EACpD,MAAM,UAAU,cAAc,KAAK;AACnC,OAAK,gBAAgB;GACnB,SAAS,QAAQ;GACjB,SAAS,QAAQ;GACjB,WAAW,QAAQ;GACnB,SAAS,QAAQ;GACjB,OAAO,QAAQ;GAChB,CAAC;AACF,YAAQ,QACN,SAAS,QAAQ,QAAQ,SAAS,QAAQ,YAAY,IAAI,KAAK,IAAI,IAC9D,QAAQ,QAAQ,SAAS,QAAQ,YAAY,IAAI,KAAK,IAAI,IAC1D,QAAQ,UAAU,eAAe,QAAQ,cAAc,IAAI,KAAK,IAAI,IACpE,QAAQ,QAAQ,SAAS,QAAQ,YAAY,IAAI,KAAK,MAC5D;AACD,SAAO,KAAK,KAAK;GACf,QAAQ;GACR,MAAM;IACJ,GAAG;IACH,SAAS,uBAAuB,KAAK;IACrC,UAAU,oBAAoB,KAAK;IACpC;GACD,QAAQ,WAAW,MAAM,MAAM;GAChC,CAAC"}
1
+ {"version":3,"file":"apply.mjs","names":[],"sources":["../../src/commands/apply.ts"],"sourcesContent":["import { Flags } from \"@oclif/core\";\nimport { consola } from \"consola\";\n\nimport { createZitadelClient } from \"@zitadel/api/client\";\n\nimport { BaseCommand, type JsonEnvelope } from \"../lib/oclif\";\nimport { environmentSchema } from \"../lib/environment\";\nimport {\n buildSyncPlan,\n collectPlanWarnings,\n enumeratePlanResources,\n makeSyncers,\n renderPlan,\n runSyncLoop,\n summarizePlan,\n} from \"../lib/sync\";\nimport { readZitadelSecret } from \"../lib/project\";\nimport { publicCliCommand } from \"../lib/public-cli\";\n\n/**\n * `zitadel apply` — validate and upload repo config to the platform.\n *\n * Runs the sync loop to convergence, or (with `--dry-run`) previews the diff\n * without mutating. All validation — structural shape and `${VAR}` / `*_env`\n * reference presence — happens inside the sync engine ({@link buildSyncPlan}),\n * so an invalid or under-configured file fails with `E_VALIDATION` before any\n * platform call.\n */\nexport default class Apply extends BaseCommand {\n static override description = \"Validate and upload repo config to the platform.\";\n static override flags = {\n environment: Flags.string({\n char: \"e\",\n description: \"Target environment (default: development).\",\n options: [...environmentSchema.options],\n }),\n };\n\n async run(): Promise<JsonEnvelope> {\n const { flags } = await this.parse(Apply);\n await this.toMeta(flags);\n const { cwd, source, env, dryRun, isTTY } = this.meta;\n\n const secret = await readZitadelSecret(cwd);\n consola.info(`Project ${secret.project_id}`);\n consola.info(`Server ${source}`);\n const client = createZitadelClient({\n baseUrl: source,\n token: secret.project_secret,\n });\n const syncers = makeSyncers({\n client,\n projectId: secret.project_id,\n env,\n cwd,\n });\n\n if (!dryRun) {\n consola.start(\"Syncing schemas and flows to Zitadel\");\n const { filesUpdated, applied } = await runSyncLoop(cwd, syncers);\n consola.success(\"Sync complete\");\n return this.emit({\n status: \"ok\",\n data: {\n synced: true,\n // Platform resources this run touched (with resulting ids);\n // `files_updated` stays the local write-backs only.\n changes: applied,\n files_updated: filesUpdated,\n next_actions:\n applied.length === 0\n ? [\"Everything is already in sync — no changes were applied.\"]\n : [\n \"Changes are live — reload your app to see them.\",\n \"Re-run plan to confirm local config and platform are in sync.\",\n ],\n next_commands: [publicCliCommand(\"plan\", this.meta.cliVersion)],\n },\n });\n }\n\n consola.start(\"Building plan (dry run)\");\n const plan = await buildSyncPlan(cwd, syncers, true);\n const summary = summarizePlan(plan);\n this.recordTelemetry({\n creates: summary.creates,\n updates: summary.updates,\n revisions: summary.revisions,\n deletes: summary.deletes,\n total: summary.total,\n });\n consola.success(\n `Plan: ${summary.creates} create${summary.creates === 1 ? \"\" : \"s\"}, ` +\n `${summary.updates} update${summary.updates === 1 ? \"\" : \"s\"}, ` +\n `${summary.revisions} new revision${summary.revisions === 1 ? \"\" : \"s\"}, ` +\n `${summary.deletes} delete${summary.deletes === 1 ? \"\" : \"s\"}`,\n );\n return this.emit({\n status: \"ok\",\n data: {\n ...summary,\n changes: enumeratePlanResources(plan),\n warnings: collectPlanWarnings(plan),\n },\n pretty: renderPlan(plan, isTTY),\n });\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA4BA,IAAqB,QAArB,MAAqB,cAAc,YAAY;CAC7C,OAAgB,cAAc;CAC9B,OAAgB,QAAQ,EACtB,aAAa,MAAM,OAAO;EACxB,MAAM;EACN,aAAa;EACb,SAAS,CAAC,GAAG,kBAAkB,QAAQ;EACxC,CAAC,EACH;CAED,MAAM,MAA6B;EACjC,MAAM,EAAE,UAAU,MAAM,KAAK,MAAM,MAAM;AACzC,QAAM,KAAK,OAAO,MAAM;EACxB,MAAM,EAAE,KAAK,QAAQ,KAAK,QAAQ,UAAU,KAAK;EAEjD,MAAM,SAAS,MAAM,kBAAkB,IAAI;AAC3C,YAAQ,KAAK,aAAa,OAAO,aAAa;AAC9C,YAAQ,KAAK,aAAa,SAAS;EAKnC,MAAM,UAAU,YAAY;GAC1B,QALa,oBAAoB;IACjC,SAAS;IACT,OAAO,OAAO;IACf,CAEO;GACN,WAAW,OAAO;GAClB;GACA;GACD,CAAC;AAEF,MAAI,CAAC,QAAQ;AACX,aAAQ,MAAM,uCAAuC;GACrD,MAAM,EAAE,cAAc,YAAY,MAAM,YAAY,KAAK,QAAQ;AACjE,aAAQ,QAAQ,gBAAgB;AAChC,UAAO,KAAK,KAAK;IACf,QAAQ;IACR,MAAM;KACJ,QAAQ;KAGR,SAAS;KACT,eAAe;KACf,cACE,QAAQ,WAAW,IACf,CAAC,2DAA2D,GAC5D,CACE,mDACA,gEACD;KACP,eAAe,CAAC,iBAAiB,QAAQ,KAAK,KAAK,WAAW,CAAC;KAChE;IACF,CAAC;;AAGJ,YAAQ,MAAM,0BAA0B;EACxC,MAAM,OAAO,MAAM,cAAc,KAAK,SAAS,KAAK;EACpD,MAAM,UAAU,cAAc,KAAK;AACnC,OAAK,gBAAgB;GACnB,SAAS,QAAQ;GACjB,SAAS,QAAQ;GACjB,WAAW,QAAQ;GACnB,SAAS,QAAQ;GACjB,OAAO,QAAQ;GAChB,CAAC;AACF,YAAQ,QACN,SAAS,QAAQ,QAAQ,SAAS,QAAQ,YAAY,IAAI,KAAK,IAAI,IAC9D,QAAQ,QAAQ,SAAS,QAAQ,YAAY,IAAI,KAAK,IAAI,IAC1D,QAAQ,UAAU,eAAe,QAAQ,cAAc,IAAI,KAAK,IAAI,IACpE,QAAQ,QAAQ,SAAS,QAAQ,YAAY,IAAI,KAAK,MAC5D;AACD,SAAO,KAAK,KAAK;GACf,QAAQ;GACR,MAAM;IACJ,GAAG;IACH,SAAS,uBAAuB,KAAK;IACrC,UAAU,oBAAoB,KAAK;IACpC;GACD,QAAQ,WAAW,MAAM,MAAM;GAChC,CAAC"}
@@ -0,0 +1,104 @@
1
+ import { t as BRANDING_DESIGN_INFO } from "../../designs-Ckz18Dpo.mjs";
2
+ import { E as ZitadelError, O as normalizePublicCliJson, T as stableStringify, j as publicCliCommand, k as normalizePublicCliProse, t as BaseCommand } from "../../oclif-CanO3zdt.mjs";
3
+ import { t as hasZitadelConfig } from "../../project-CHhot85s.mjs";
4
+ import { t as BRANDING_DIR } from "../../branding-cygSBPkV.mjs";
5
+ import { mkdir, writeFile } from "node:fs/promises";
6
+ import { dirname, join } from "node:path";
7
+ import { cancel, isCancel, select } from "@clack/prompts";
8
+ import { Flags } from "@oclif/core";
9
+ import { BRANDING_DESIGNS, DEFAULT_BRANDING_CONFIG_PATH, DEFAULT_BRANDING_DESIGN, DEFAULT_BRANDING_TEMPLATE_PATH, brandingReadmeContent, getDefaultBrandingConfig } from "@zitadel/config/defaults";
10
+ import { consola as consola$1 } from "consola";
11
+ import { BRANDING_FILE_SCHEMA_REF, META_SCHEMA_DIR, metaSchemaFiles } from "@zitadel/config/meta-schemas";
12
+ //#region src/commands/branding/eject.ts
13
+ /**
14
+ * The `branding eject` command — take ownership of the login template.
15
+ *
16
+ * Scaffolds `.zitadel/branding/` from a shipped design: the `branding.json`
17
+ * descriptor, the `login.liquid` template it references, a README, and the
18
+ * branding dialect meta-schema (for projects set up before it existed).
19
+ * Purely local — nothing is uploaded; the first `zitadel apply` publishes
20
+ * revision 1. Ejecting the template is the last rung of the branding
21
+ * override ladder (docs/design/branding/override-ladder.md); unrelated to
22
+ * `zitadel eject`, which removes the Zitadel integration.
23
+ */
24
+ var BrandingEject = class BrandingEject extends BaseCommand {
25
+ static description = "Take ownership of the login template: scaffold .zitadel/branding/ from a shipped design.";
26
+ static flags = { design: Flags.string({
27
+ description: `Design to start from (default: ${DEFAULT_BRANDING_DESIGN}).`,
28
+ options: [...BRANDING_DESIGNS]
29
+ }) };
30
+ async run() {
31
+ const { flags } = await this.parse(BrandingEject);
32
+ await this.toMeta(flags);
33
+ const { cwd, force, nonInteractive } = this.meta;
34
+ if (!await hasZitadelConfig(cwd)) throw new ZitadelError("E_VALIDATION", "This directory is not a Zitadel project", {
35
+ hint: "Run setup first; branding files live under .zitadel/.",
36
+ nextCommands: [publicCliCommand("setup", this.meta.cliVersion)]
37
+ });
38
+ const design = flags.design ?? await this.promptDesign(nonInteractive);
39
+ this.recordTelemetry({
40
+ design,
41
+ force
42
+ });
43
+ const { branding, template } = getDefaultBrandingConfig(design);
44
+ const filesWritten = [];
45
+ const write = async (relPath, content, overwrite) => {
46
+ const dest = join(cwd, relPath);
47
+ await mkdir(dirname(dest), { recursive: true });
48
+ try {
49
+ await writeFile(dest, content, overwrite ? void 0 : { flag: "wx" });
50
+ filesWritten.push(relPath);
51
+ } catch (error) {
52
+ if (isErrno(error, "EEXIST")) throw new ZitadelError("E_CONFLICT", `${relPath} already exists`, { hint: "Rerun with --force to replace it, or edit the existing file directly." });
53
+ throw error;
54
+ }
55
+ };
56
+ await write(DEFAULT_BRANDING_CONFIG_PATH, `${stableStringify({
57
+ $schema: BRANDING_FILE_SCHEMA_REF,
58
+ ...branding
59
+ })}\n`, force);
60
+ await write(DEFAULT_BRANDING_TEMPLATE_PATH, template, force);
61
+ await write(join(BRANDING_DIR, "README.md"), normalizePublicCliProse(brandingReadmeContent(), this.meta.cliVersion), false).catch(swallowConflict);
62
+ const brandingMeta = metaSchemaFiles().find((f) => f.name === "branding.json");
63
+ if (brandingMeta) await write(join(META_SCHEMA_DIR, brandingMeta.name), `${stableStringify(normalizePublicCliJson(brandingMeta.body, this.meta.cliVersion))}\n`, false).catch(swallowConflict);
64
+ for (const file of filesWritten) consola$1.info(`Wrote ${file}`);
65
+ consola$1.success(`Ejected the ${design} design into ${BRANDING_DIR}/`);
66
+ return this.emit({
67
+ status: "ok",
68
+ data: {
69
+ design,
70
+ files_written: filesWritten,
71
+ next_actions: [`Edit ${DEFAULT_BRANDING_TEMPLATE_PATH} — the login UI is yours now.`, "Plan validates the template and shows the pending revision; apply publishes it."],
72
+ next_commands: [publicCliCommand("plan", this.meta.cliVersion), publicCliCommand("apply", this.meta.cliVersion)]
73
+ }
74
+ });
75
+ }
76
+ async promptDesign(nonInteractive) {
77
+ if (nonInteractive) return DEFAULT_BRANDING_DESIGN;
78
+ const choice = await select({
79
+ message: "Which design do you want to start from?",
80
+ initialValue: DEFAULT_BRANDING_DESIGN,
81
+ options: BRANDING_DESIGNS.map((design) => ({
82
+ value: design,
83
+ label: BRANDING_DESIGN_INFO[design].label,
84
+ hint: BRANDING_DESIGN_INFO[design].hint
85
+ }))
86
+ });
87
+ if (isCancel(choice)) {
88
+ cancel("Eject cancelled.");
89
+ throw new ZitadelError("E_VALIDATION", "Eject cancelled by user");
90
+ }
91
+ return choice;
92
+ }
93
+ };
94
+ function swallowConflict(error) {
95
+ if (error instanceof ZitadelError && error.code === "E_CONFLICT") return;
96
+ throw error;
97
+ }
98
+ function isErrno(error, code) {
99
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
100
+ }
101
+ //#endregion
102
+ export { BrandingEject as default };
103
+
104
+ //# sourceMappingURL=eject.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eject.mjs","names":[],"sources":["../../../src/commands/branding/eject.ts"],"sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\n\nimport { cancel, isCancel, select } from \"@clack/prompts\";\nimport { Flags } from \"@oclif/core\";\nimport { consola } from \"consola\";\n\nimport {\n BRANDING_DESIGNS,\n DEFAULT_BRANDING_CONFIG_PATH,\n DEFAULT_BRANDING_DESIGN,\n DEFAULT_BRANDING_TEMPLATE_PATH,\n brandingReadmeContent,\n getDefaultBrandingConfig,\n} from \"@zitadel/config/defaults\";\nimport { BRANDING_FILE_SCHEMA_REF, META_SCHEMA_DIR, metaSchemaFiles } from \"@zitadel/config/meta-schemas\";\n\nimport { BRANDING_DIR } from \"../../lib/branding\";\nimport { BRANDING_DESIGN_INFO } from \"../../lib/branding/designs\";\nimport { ZitadelError } from \"../../lib/errors\";\nimport { stableStringify } from \"../../lib/json\";\nimport { BaseCommand, type JsonEnvelope } from \"../../lib/oclif\";\nimport { hasZitadelConfig } from \"../../lib/project\";\nimport {\n normalizePublicCliJson,\n normalizePublicCliProse,\n publicCliCommand,\n} from \"../../lib/public-cli\";\n\n/**\n * The `branding eject` command — take ownership of the login template.\n *\n * Scaffolds `.zitadel/branding/` from a shipped design: the `branding.json`\n * descriptor, the `login.liquid` template it references, a README, and the\n * branding dialect meta-schema (for projects set up before it existed).\n * Purely local — nothing is uploaded; the first `zitadel apply` publishes\n * revision 1. Ejecting the template is the last rung of the branding\n * override ladder (docs/design/branding/override-ladder.md); unrelated to\n * `zitadel eject`, which removes the Zitadel integration.\n */\nexport default class BrandingEject extends BaseCommand {\n static override description =\n \"Take ownership of the login template: scaffold .zitadel/branding/ from a shipped design.\";\n static override flags = {\n design: Flags.string({\n description: `Design to start from (default: ${DEFAULT_BRANDING_DESIGN}).`,\n options: [...BRANDING_DESIGNS],\n }),\n };\n\n async run(): Promise<JsonEnvelope> {\n const { flags } = await this.parse(BrandingEject);\n await this.toMeta(flags);\n const { cwd, force, nonInteractive } = this.meta;\n\n if (!(await hasZitadelConfig(cwd))) {\n throw new ZitadelError(\"E_VALIDATION\", \"This directory is not a Zitadel project\", {\n hint: \"Run setup first; branding files live under .zitadel/.\",\n nextCommands: [publicCliCommand(\"setup\", this.meta.cliVersion)],\n });\n }\n\n const design = flags.design ?? (await this.promptDesign(nonInteractive));\n this.recordTelemetry({ design, force });\n const { branding, template } = getDefaultBrandingConfig(design);\n\n const filesWritten: string[] = [];\n const write = async (relPath: string, content: string, overwrite: boolean): Promise<void> => {\n const dest = join(cwd, relPath);\n await mkdir(dirname(dest), { recursive: true });\n try {\n await writeFile(dest, content, overwrite ? undefined : { flag: \"wx\" });\n filesWritten.push(relPath);\n } catch (error) {\n if (isErrno(error, \"EEXIST\")) {\n throw new ZitadelError(\"E_CONFLICT\", `${relPath} already exists`, {\n hint: \"Rerun with --force to replace it, or edit the existing file directly.\",\n });\n }\n throw error;\n }\n };\n\n await write(\n DEFAULT_BRANDING_CONFIG_PATH,\n `${stableStringify({ $schema: BRANDING_FILE_SCHEMA_REF, ...branding })}\\n`,\n force,\n );\n await write(DEFAULT_BRANDING_TEMPLATE_PATH, template, force);\n\n // Never overwrite a hand-edited README; setups from before the branding\n // dialect also miss the meta-schema, so materialize it when absent.\n await write(\n join(BRANDING_DIR, \"README.md\"),\n normalizePublicCliProse(brandingReadmeContent(), this.meta.cliVersion),\n false,\n ).catch(swallowConflict);\n const brandingMeta = metaSchemaFiles().find((f) => f.name === \"branding.json\");\n if (brandingMeta) {\n await write(\n join(META_SCHEMA_DIR, brandingMeta.name),\n `${stableStringify(normalizePublicCliJson(brandingMeta.body, this.meta.cliVersion))}\\n`,\n false,\n ).catch(swallowConflict);\n }\n\n for (const file of filesWritten) {\n consola.info(`Wrote ${file}`);\n }\n consola.success(`Ejected the ${design} design into ${BRANDING_DIR}/`);\n\n return this.emit({\n status: \"ok\",\n data: {\n design,\n files_written: filesWritten,\n next_actions: [\n `Edit ${DEFAULT_BRANDING_TEMPLATE_PATH} — the login UI is yours now.`,\n \"Plan validates the template and shows the pending revision; apply publishes it.\",\n ],\n next_commands: [\n publicCliCommand(\"plan\", this.meta.cliVersion),\n publicCliCommand(\"apply\", this.meta.cliVersion),\n ],\n },\n });\n }\n\n private async promptDesign(nonInteractive: boolean): Promise<string> {\n if (nonInteractive) {\n return DEFAULT_BRANDING_DESIGN;\n }\n const choice = await select({\n message: \"Which design do you want to start from?\",\n initialValue: DEFAULT_BRANDING_DESIGN as string,\n options: BRANDING_DESIGNS.map((design) => ({\n value: design as string,\n label: BRANDING_DESIGN_INFO[design].label,\n hint: BRANDING_DESIGN_INFO[design].hint,\n })),\n });\n if (isCancel(choice)) {\n cancel(\"Eject cancelled.\");\n throw new ZitadelError(\"E_VALIDATION\", \"Eject cancelled by user\");\n }\n return choice;\n }\n}\n\nfunction swallowConflict(error: unknown): void {\n if (error instanceof ZitadelError && error.code === \"E_CONFLICT\") {\n return;\n }\n throw error;\n}\n\nfunction isErrno(error: unknown, code: string): boolean {\n return (\n typeof error === \"object\" &&\n error !== null &&\n \"code\" in error &&\n (error as NodeJS.ErrnoException).code === code\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAwCA,IAAqB,gBAArB,MAAqB,sBAAsB,YAAY;CACrD,OAAgB,cACd;CACF,OAAgB,QAAQ,EACtB,QAAQ,MAAM,OAAO;EACnB,aAAa,kCAAkC,wBAAwB;EACvE,SAAS,CAAC,GAAG,iBAAiB;EAC/B,CAAC,EACH;CAED,MAAM,MAA6B;EACjC,MAAM,EAAE,UAAU,MAAM,KAAK,MAAM,cAAc;AACjD,QAAM,KAAK,OAAO,MAAM;EACxB,MAAM,EAAE,KAAK,OAAO,mBAAmB,KAAK;AAE5C,MAAI,CAAE,MAAM,iBAAiB,IAAI,CAC/B,OAAM,IAAI,aAAa,gBAAgB,2CAA2C;GAChF,MAAM;GACN,cAAc,CAAC,iBAAiB,SAAS,KAAK,KAAK,WAAW,CAAC;GAChE,CAAC;EAGJ,MAAM,SAAS,MAAM,UAAW,MAAM,KAAK,aAAa,eAAe;AACvE,OAAK,gBAAgB;GAAE;GAAQ;GAAO,CAAC;EACvC,MAAM,EAAE,UAAU,aAAa,yBAAyB,OAAO;EAE/D,MAAM,eAAyB,EAAE;EACjC,MAAM,QAAQ,OAAO,SAAiB,SAAiB,cAAsC;GAC3F,MAAM,OAAO,KAAK,KAAK,QAAQ;AAC/B,SAAM,MAAM,QAAQ,KAAK,EAAE,EAAE,WAAW,MAAM,CAAC;AAC/C,OAAI;AACF,UAAM,UAAU,MAAM,SAAS,YAAY,KAAA,IAAY,EAAE,MAAM,MAAM,CAAC;AACtE,iBAAa,KAAK,QAAQ;YACnB,OAAO;AACd,QAAI,QAAQ,OAAO,SAAS,CAC1B,OAAM,IAAI,aAAa,cAAc,GAAG,QAAQ,kBAAkB,EAChE,MAAM,yEACP,CAAC;AAEJ,UAAM;;;AAIV,QAAM,MACJ,8BACA,GAAG,gBAAgB;GAAE,SAAS;GAA0B,GAAG;GAAU,CAAC,CAAC,KACvE,MACD;AACD,QAAM,MAAM,gCAAgC,UAAU,MAAM;AAI5D,QAAM,MACJ,KAAK,cAAc,YAAY,EAC/B,wBAAwB,uBAAuB,EAAE,KAAK,KAAK,WAAW,EACtE,MACD,CAAC,MAAM,gBAAgB;EACxB,MAAM,eAAe,iBAAiB,CAAC,MAAM,MAAM,EAAE,SAAS,gBAAgB;AAC9E,MAAI,aACF,OAAM,MACJ,KAAK,iBAAiB,aAAa,KAAK,EACxC,GAAG,gBAAgB,uBAAuB,aAAa,MAAM,KAAK,KAAK,WAAW,CAAC,CAAC,KACpF,MACD,CAAC,MAAM,gBAAgB;AAG1B,OAAK,MAAM,QAAQ,aACjB,WAAQ,KAAK,SAAS,OAAO;AAE/B,YAAQ,QAAQ,eAAe,OAAO,eAAe,aAAa,GAAG;AAErE,SAAO,KAAK,KAAK;GACf,QAAQ;GACR,MAAM;IACJ;IACA,eAAe;IACf,cAAc,CACZ,QAAQ,+BAA+B,gCACvC,kFACD;IACD,eAAe,CACb,iBAAiB,QAAQ,KAAK,KAAK,WAAW,EAC9C,iBAAiB,SAAS,KAAK,KAAK,WAAW,CAChD;IACF;GACF,CAAC;;CAGJ,MAAc,aAAa,gBAA0C;AACnE,MAAI,eACF,QAAO;EAET,MAAM,SAAS,MAAM,OAAO;GAC1B,SAAS;GACT,cAAc;GACd,SAAS,iBAAiB,KAAK,YAAY;IACzC,OAAO;IACP,OAAO,qBAAqB,QAAQ;IACpC,MAAM,qBAAqB,QAAQ;IACpC,EAAE;GACJ,CAAC;AACF,MAAI,SAAS,OAAO,EAAE;AACpB,UAAO,mBAAmB;AAC1B,SAAM,IAAI,aAAa,gBAAgB,0BAA0B;;AAEnE,SAAO;;;AAIX,SAAS,gBAAgB,OAAsB;AAC7C,KAAI,iBAAiB,gBAAgB,MAAM,SAAS,aAClD;AAEF,OAAM;;AAGR,SAAS,QAAQ,OAAgB,MAAuB;AACtD,QACE,OAAO,UAAU,YACjB,UAAU,QACV,UAAU,SACT,MAAgC,SAAS"}