@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
@@ -1,19 +1,51 @@
1
- import { D as ZitadelError, E as stableStringify, O as toZitadelError, n as DEFAULT_SERVER, t as BaseCommand, u as detectHealthyLocalServer, x as publicCliCommand } from "../oclif-BRdjVNne.mjs";
2
- import { n as inspectScaffoldTarget, o as issuerFromPort, r as RENDERER_IDS, t as createOrca } from "../orca-Dip6DPJ2.mjs";
3
- import { n as hasZitadelSecret, t as hasZitadelConfig } from "../project-BGgv7Bba.mjs";
4
- import { c as writeBackResource, d as FLOWS_DIR, l as updateState, o as hashForState } from "../sync-Cd3z7Q1R.mjs";
5
- import { t as SCHEMAS_DIR } from "../user-schema-DDz5-lX5.mjs";
6
- import { n as verifyLoginAction, t as customizeAndPublishActions } from "../journey-guidance-CYVpQszz.mjs";
1
+ import { n as brandingDesignLabel, t as BRANDING_DESIGN_INFO } from "../designs-Ckz18Dpo.mjs";
2
+ import { D as toZitadelError, E as ZitadelError, T as stableStringify, j as publicCliCommand, k as normalizePublicCliProse, n as DEFAULT_SERVER, t as BaseCommand, u as detectHealthyLocalServer } from "../oclif-CanO3zdt.mjs";
3
+ import { n as claimCommand, r as claimState, t as claimAction } from "../claim-state-DlfD6Res.mjs";
4
+ import { n as customizeAndPublishActions, r as verifyLoginAction, t as brandingGuidanceAction } from "../journey-guidance-BGrOX_gT.mjs";
5
+ import { i as RENDERER_IDS, n as inspectScaffoldTarget, r as AVAILABLE_RENDERER_IDS, s as issuerFromPort, t as createOrca } from "../orca-Cu0iW1mv.mjs";
6
+ import { n as hasZitadelSecret, t as hasZitadelConfig } from "../project-CHhot85s.mjs";
7
+ import { a as updateState, t as SCHEMAS_DIR } from "../user-schema-DTuOsdKE.mjs";
8
+ import { a as runPackageCommand, c as writeScaffoldManifest, i as installCommandFor, n as detectPackageManager, r as devCommandFor } from "../package-manager-DFdLcDx1.mjs";
9
+ import { i as toBrandingWireBody, t as BRANDING_DIR } from "../branding-cygSBPkV.mjs";
10
+ import { c as writeBackResource, o as hashForState, u as FLOWS_DIR } from "../sync-Bd6vASgg.mjs";
7
11
  import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
8
12
  import { basename, dirname, join } from "node:path";
9
13
  import { cancel, confirm, intro, isCancel, outro, select, spinner, text } from "@clack/prompts";
10
14
  import { Flags } from "@oclif/core";
11
15
  import { createZitadelClient } from "@zitadel/api/client";
12
- import { DEFAULT_FLOW_CONFIG_PATH, DEFAULT_FLOW_SCHEMA_URI, DEFAULT_SCHEMA_CONFIG_PATH, DEFAULT_SETUP_PRESET, SETUP_PRESETS, flowsReadmeContent, getDefaultHumanUserSchema, getDefaultLoginFlow, schemasReadmeContent } from "@zitadel/config/defaults";
16
+ import { BRANDING_DESIGNS, DEFAULT_BRANDING_CONFIG_PATH, DEFAULT_BRANDING_TEMPLATE_PATH, DEFAULT_FLOW_CONFIG_PATH, DEFAULT_FLOW_SCHEMA_URI, DEFAULT_SCHEMA_CONFIG_PATH, DEFAULT_SETUP_PRESET, DEFAULT_SETUP_USE_CASE, SETUP_PRESETS, SETUP_USE_CASES, brandingReadmeContent, flowsReadmeContent, getDefaultBrandingConfig, getDefaultHumanUserSchema, getDefaultLoginFlow, schemasReadmeContent } from "@zitadel/config/defaults";
13
17
  import { consola as consola$1 } from "consola";
14
- import { spawn } from "node:child_process";
18
+ import { BRANDING_FILE_SCHEMA_REF } from "@zitadel/config/meta-schemas";
15
19
  import { normalizeFlowBody, normalizeSchemaBody } from "@zitadel/config/normalize";
16
20
  import pc from "picocolors";
21
+ //#region src/lib/orca/patchers/posture.ts
22
+ /**
23
+ * Frameworks whose patchers add route files without owning the app shell —
24
+ * the only ones where a pre-existing app keeps a layout for the widget
25
+ * posture to inherit (ADR 044). The SPA families write the app's root
26
+ * component, so nothing survives for a widget to embed into; they keep the
27
+ * page posture until a non-destructive route/layout insertion contract
28
+ * exists for their routers.
29
+ */
30
+ const ROUTE_BASED_FRAMEWORKS = new Set(["next", "nuxt"]);
31
+ /**
32
+ * The default embedding posture of the scaffolded auth/profile pages
33
+ * (ADR 044), derived from the same hinge as the framework homepage: whether
34
+ * setup created the app skeleton itself. A fresh scaffold has no design to
35
+ * respect — full-page chrome is the strongest start. A pre-existing
36
+ * route-based app has its own shell and theme, so the pages embed
37
+ * `variant="widget"` cards in a layout-neutral wrapper instead of painting
38
+ * token-colored chrome underneath the host layout.
39
+ *
40
+ * Derived once at setup time and recorded in the scaffold manifest;
41
+ * `doctor --fix` restores from the record rather than re-deriving (a
42
+ * manifest-less legacy scaffold could not answer the hinge).
43
+ */
44
+ function derivePosture(frameworkId, scaffoldedFramework) {
45
+ if (!ROUTE_BASED_FRAMEWORKS.has(frameworkId)) return "page";
46
+ return scaffoldedFramework ? "page" : "widget";
47
+ }
48
+ //#endregion
17
49
  //#region src/lib/setup-resources.ts
18
50
  /**
19
51
  * Scaffolds the versioned local default resources for a new project, uploads
@@ -31,12 +63,16 @@ async function materializeSetupResources(opts) {
31
63
  await mkdir(join(opts.cwd, SCHEMAS_DIR), { recursive: true });
32
64
  const filesWritten = [];
33
65
  const preset = opts.preset ?? DEFAULT_SETUP_PRESET;
34
- const { $id: _templateId, ...schemaBody } = getDefaultHumanUserSchema({ preset });
66
+ const useCase = opts.useCase ?? DEFAULT_SETUP_USE_CASE;
67
+ const { $id: _templateId, ...schemaBody } = getDefaultHumanUserSchema({
68
+ preset,
69
+ useCase
70
+ });
35
71
  if (await writeResourceFile(opts.cwd, DEFAULT_SCHEMA_CONFIG_PATH, schemaBody, opts.force)) filesWritten.push(join(opts.cwd, DEFAULT_SCHEMA_CONFIG_PATH));
36
72
  const schemaId = requiredString((await opts.client.createSchema(schemaBody, { project_id: opts.projectId })).id, "created schema id");
37
73
  let schemaHash = hashForState({ normalize: normalizeSchemaBody }, schemaBody);
38
74
  try {
39
- const canonical = await opts.client.getSchemaById(encodeURIComponent(schemaId), { project_id: opts.projectId });
75
+ const canonical = await opts.client.getSchemaById(encodeURIComponent(schemaId));
40
76
  schemaHash = (await writeBackResource(opts.cwd, DEFAULT_SCHEMA_CONFIG_PATH, { normalize: normalizeSchemaBody }, canonical)).hash;
41
77
  } catch (err) {
42
78
  consola$1.debug(`fetch created schema ${schemaId} during setup failed:`, err);
@@ -47,7 +83,8 @@ async function materializeSetupResources(opts) {
47
83
  });
48
84
  const flowBody = getDefaultLoginFlow({
49
85
  userSchemaUrl: schemaId,
50
- preset
86
+ preset,
87
+ useCase
51
88
  });
52
89
  if (await writeResourceFile(opts.cwd, DEFAULT_FLOW_CONFIG_PATH, flowBody, opts.force)) filesWritten.push(join(opts.cwd, DEFAULT_FLOW_CONFIG_PATH));
53
90
  const flow = await opts.client.createFlowDefinition({
@@ -66,13 +103,47 @@ async function materializeSetupResources(opts) {
66
103
  name: flowBody.name,
67
104
  status: flowBody.status
68
105
  });
106
+ if (opts.design) {
107
+ await mkdir(join(opts.cwd, BRANDING_DIR), { recursive: true });
108
+ const { branding, template } = getDefaultBrandingConfig(opts.design);
109
+ const descriptor = {
110
+ $schema: BRANDING_FILE_SCHEMA_REF,
111
+ ...branding
112
+ };
113
+ if (await writeResourceFile(opts.cwd, DEFAULT_BRANDING_CONFIG_PATH, descriptor, opts.force)) filesWritten.push(join(opts.cwd, DEFAULT_BRANDING_CONFIG_PATH));
114
+ if (await writeRawFile(opts.cwd, DEFAULT_BRANDING_TEMPLATE_PATH, template, opts.force)) filesWritten.push(join(opts.cwd, DEFAULT_BRANDING_TEMPLATE_PATH));
115
+ const brandingNormalize = (data) => toBrandingWireBody(opts.cwd, data);
116
+ const created = await opts.client.createBranding(brandingNormalize(descriptor), { project_id: opts.projectId });
117
+ await updateState(opts.cwd, DEFAULT_BRANDING_CONFIG_PATH, {
118
+ id: requiredString(created.id, "created branding revision id"),
119
+ hash: hashForState({ normalize: brandingNormalize }, descriptor)
120
+ });
121
+ const brandingReadme = join(BRANDING_DIR, "README.md");
122
+ if (await writeReadmeFile(opts.cwd, brandingReadme, normalizePublicCliProse(brandingReadmeContent(), opts.cliVersion))) filesWritten.push(join(opts.cwd, brandingReadme));
123
+ }
69
124
  const schemasReadme = join(SCHEMAS_DIR, "README.md");
70
125
  const flowsReadme = join(FLOWS_DIR, "README.md");
71
- if (await writeReadmeFile(opts.cwd, schemasReadme, schemasReadmeContent())) filesWritten.push(join(opts.cwd, schemasReadme));
72
- if (await writeReadmeFile(opts.cwd, flowsReadme, flowsReadmeContent())) filesWritten.push(join(opts.cwd, flowsReadme));
126
+ if (await writeReadmeFile(opts.cwd, schemasReadme, normalizePublicCliProse(schemasReadmeContent(), opts.cliVersion))) filesWritten.push(join(opts.cwd, schemasReadme));
127
+ if (await writeReadmeFile(opts.cwd, flowsReadme, normalizePublicCliProse(flowsReadmeContent(), opts.cliVersion))) filesWritten.push(join(opts.cwd, flowsReadme));
73
128
  return { filesWritten };
74
129
  }
75
130
  /**
131
+ * Write a non-JSON scaffold file (the `.liquid` template) with the same
132
+ * conflict semantics as {@link writeResourceFile}: `--force` overwrites,
133
+ * otherwise an existing file is an `E_CONFLICT`.
134
+ */
135
+ async function writeRawFile(cwd, relPath, content, force) {
136
+ const dest = join(cwd, relPath);
137
+ await mkdir(dirname(dest), { recursive: true });
138
+ try {
139
+ await writeFile(dest, content, force ? void 0 : { flag: "wx" });
140
+ return true;
141
+ } catch (error) {
142
+ if (isErrno(error, "EEXIST")) throw new ZitadelError("E_CONFLICT", `${relPath} already exists`, { hint: "Move the file aside or rerun setup with --force if you want setup to replace it." });
143
+ throw error;
144
+ }
145
+ }
146
+ /**
76
147
  * Write a README file, but never overwrite an existing one. A developer who
77
148
  * has edited the README should keep their edits when `setup --force` is
78
149
  * re-run.
@@ -106,88 +177,6 @@ function isErrno(error, code) {
106
177
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
107
178
  }
108
179
  //#endregion
109
- //#region src/lib/package-manager.ts
110
- async function detectPackageManager(cwd) {
111
- const declared = await packageManagerFromManifest(cwd);
112
- if (declared) return declared;
113
- if (await exists(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
114
- if (await exists(join(cwd, "yarn.lock"))) return "yarn";
115
- if (await exists(join(cwd, "bun.lock"))) return "bun";
116
- if (await exists(join(cwd, "bun.lockb"))) return "bun";
117
- if (await exists(join(cwd, "package-lock.json"))) return "npm";
118
- return "npm";
119
- }
120
- function installCommandFor(packageManager) {
121
- return command(packageManager, ["install"]);
122
- }
123
- function devCommandFor(packageManager) {
124
- switch (packageManager) {
125
- case "npm": return command("npm", ["run", "dev"]);
126
- case "pnpm": return command("pnpm", ["dev"]);
127
- case "yarn": return command("yarn", ["dev"]);
128
- case "bun": return command("bun", ["run", "dev"]);
129
- }
130
- }
131
- async function runPackageCommand(packageCommand, options) {
132
- await new Promise((resolve, reject) => {
133
- const child = spawn(packageCommand.command, packageCommand.args, {
134
- cwd: options.cwd,
135
- env: options.env ?? process.env,
136
- stdio: options.redirectStdoutToStderr ? [
137
- "ignore",
138
- "pipe",
139
- "pipe"
140
- ] : "inherit"
141
- });
142
- if (options.redirectStdoutToStderr) {
143
- child.stdout?.on("data", (chunk) => process.stderr.write(chunk));
144
- child.stderr?.on("data", (chunk) => process.stderr.write(chunk));
145
- }
146
- child.on("error", reject);
147
- child.on("close", (code, signal) => {
148
- if (code === 0) {
149
- resolve();
150
- return;
151
- }
152
- const detail = signal ? `signal ${signal}` : `exit ${String(code ?? 1)}`;
153
- const error = /* @__PURE__ */ new Error(`${packageCommand.display} failed with ${detail}`);
154
- Object.assign(error, {
155
- code: code ?? 1,
156
- signal
157
- });
158
- reject(error);
159
- });
160
- });
161
- }
162
- function command(packageManager, args) {
163
- return {
164
- command: packageManager,
165
- args,
166
- display: [packageManager, ...args].join(" ")
167
- };
168
- }
169
- async function packageManagerFromManifest(cwd) {
170
- try {
171
- const raw = await readFile(join(cwd, "package.json"), "utf8");
172
- const parsed = JSON.parse(raw);
173
- return typeof parsed.packageManager === "string" ? packageManagerFromString(parsed.packageManager) : void 0;
174
- } catch {
175
- return;
176
- }
177
- }
178
- function packageManagerFromString(value) {
179
- const name = value.split("@")[0];
180
- return name === "npm" || name === "pnpm" || name === "yarn" || name === "bun" ? name : void 0;
181
- }
182
- async function exists(path) {
183
- try {
184
- await stat(path);
185
- return true;
186
- } catch {
187
- return false;
188
- }
189
- }
190
- //#endregion
191
180
  //#region src/commands/setup/install.ts
192
181
  async function installDependenciesForSetup(input) {
193
182
  const packageManager = await detectPackageManager(input.cwd);
@@ -294,6 +283,54 @@ function bail(value) {
294
283
  }
295
284
  }
296
285
  //#endregion
286
+ //#region src/commands/setup/prompts/design.ts
287
+ /**
288
+ * Sentinel select value for "keep the built-in login" — clack option values
289
+ * must be strings, and `undefined` in {@link SetupAnswers.design} is the
290
+ * real representation of that choice.
291
+ */
292
+ const BUILT_IN = "built-in";
293
+ /**
294
+ * "How should the login look?" — the last wizard question (#676). Asks after
295
+ * the sign-in preset so the order tells a story: the use case owns what is
296
+ * collected, the preset owns how users authenticate, the design owns how
297
+ * that experience looks.
298
+ *
299
+ * The built-in default is preselected and writes nothing. Picking a starter
300
+ * design forks its Liquid template into `.zitadel/branding/` and publishes
301
+ * it as branding revision 1 — from then on the template is repo-owned and
302
+ * stops tracking built-in improvements, so the option labels make the
303
+ * file-write explicit and keep ownership an opt-in. `--design` is
304
+ * authoritative; non-interactive runs keep the built-in template.
305
+ */
306
+ var DesignPrompt = class {
307
+ async ask(answers, ctx) {
308
+ if (ctx.designFromFlag) return answers;
309
+ const value = await select({
310
+ message: "How should the login look? A starter design adds its editable template to .zitadel/branding/.",
311
+ initialValue: answers.design ?? BUILT_IN,
312
+ options: [{
313
+ value: BUILT_IN,
314
+ label: "Built-in",
315
+ hint: "no files added; pick a design anytime later with `branding eject`"
316
+ }, ...BRANDING_DESIGNS.map((design) => ({
317
+ value: design,
318
+ label: BRANDING_DESIGN_INFO[design].label,
319
+ hint: BRANDING_DESIGN_INFO[design].hint
320
+ }))]
321
+ });
322
+ bail(value);
323
+ if (value === BUILT_IN) return {
324
+ ...answers,
325
+ design: void 0
326
+ };
327
+ return {
328
+ ...answers,
329
+ design: value
330
+ };
331
+ }
332
+ };
333
+ //#endregion
297
334
  //#region src/commands/setup/prompts/dev-port.ts
298
335
  /**
299
336
  * "Dev server port" — defaults to the detected port. The validated answer
@@ -445,6 +482,48 @@ var SignInPresetPrompt = class {
445
482
  }
446
483
  };
447
484
  //#endregion
485
+ //#region src/commands/setup/prompts/use-case.ts
486
+ /**
487
+ * "Who will sign in to your app?" — picks the use case, which owns the
488
+ * schema field set the scaffold collects (#448). Orthogonal to the sign-in
489
+ * preset, so this asks first: what a user *is* before *how* they sign in.
490
+ * `--use-case` is authoritative; non-interactive runs keep the minimal
491
+ * default seeded by the command.
492
+ */
493
+ var UseCasePrompt = class {
494
+ async ask(answers, ctx) {
495
+ if (ctx.useCaseFromFlag) return answers;
496
+ const options = {
497
+ minimal: {
498
+ label: "Just me or a small group",
499
+ hint: "collects email"
500
+ },
501
+ consumer: {
502
+ label: "Consumers",
503
+ hint: "collects email, given & family name"
504
+ },
505
+ business: {
506
+ label: "Business",
507
+ hint: "collects email, given & family name, company"
508
+ }
509
+ };
510
+ const value = await select({
511
+ message: "Who will sign in to your app? Collected fields stay editable after setup.",
512
+ initialValue: answers.useCase,
513
+ options: SETUP_USE_CASES.map((useCase) => ({
514
+ value: useCase,
515
+ label: options[useCase].label,
516
+ hint: options[useCase].hint
517
+ }))
518
+ });
519
+ bail(value);
520
+ return {
521
+ ...answers,
522
+ useCase: value
523
+ };
524
+ }
525
+ };
526
+ //#endregion
448
527
  //#region src/commands/setup/prompts/pick-framework.ts
449
528
  /**
450
529
  * "Choose a framework to scaffold" — the only prompt outside the main wizard.
@@ -484,11 +563,23 @@ const SETUP_PROMPTS = [
484
563
  new FrameworkConfirmPrompt(),
485
564
  new ServerPrompt(),
486
565
  new DevPortPrompt(),
487
- new SignInPresetPrompt()
566
+ new UseCasePrompt(),
567
+ new SignInPresetPrompt(),
568
+ new DesignPrompt()
488
569
  ];
489
570
  //#endregion
490
571
  //#region src/commands/setup/summary.ts
491
572
  /**
573
+ * Renders a path under `cwd` with stable POSIX separators. Setup's summary
574
+ * matches generated artifacts against repository-style suffixes such as
575
+ * `.zitadel/schemas/default-human-user.json`; normalizing here keeps those
576
+ * matches working when Node returns Windows paths with backslashes.
577
+ */
578
+ function relativeDisplayPath(cwd, absolute) {
579
+ const boundary = absolute[cwd.length];
580
+ return (absolute.startsWith(cwd) && (boundary === "/" || boundary === "\\") ? absolute.slice(cwd.length + 1) : absolute).replaceAll("\\", "/");
581
+ }
582
+ /**
492
583
  * Renders the section list as a single multi-line string. Labels are
493
584
  * padded to a common width per section so the `✓ label value` columns
494
585
  * line up, the title prints in dim gray, and `✓` is green. Values come
@@ -517,6 +608,8 @@ const path = (s) => pc.cyan(s);
517
608
  const url = (s) => pc.cyan(s);
518
609
  /** Yellow, for opaque ids the user shouldn't try to read. */
519
610
  const id = (s) => pc.yellow(s);
611
+ /** Dim, for separators and secondary detail. */
612
+ const dim = (s) => pc.dim(s);
520
613
  /**
521
614
  * Reads the project root to identify the framework version, TS presence,
522
615
  * and which package manager the user runs. Returns safe defaults (npm
@@ -577,6 +670,29 @@ function stripRange(range) {
577
670
  function fileNameOf(p) {
578
671
  return basename(p);
579
672
  }
673
+ /**
674
+ * Layout caveats for the chosen login design, shown to humans after the
675
+ * summary box and carried in the JSON envelope's `warnings`.
676
+ *
677
+ * The split family's brand pane is keyed to the **login's own container**
678
+ * width — a `@container (max-width: 48rem)` query on the widget's mount, not
679
+ * a viewport media query. Above that the pane renders; below it the pane is
680
+ * `display: none` and the compact brand mark takes its place — and the split
681
+ * template only emits that mark when `branding.json` names `logo_url` or
682
+ * `hero_url`, so without one the narrow layout loses the branding entirely.
683
+ *
684
+ * Independent of posture, because the container query is: a full-page login
685
+ * hits the same collapse on a phone that an embedded card hits in a sidebar.
686
+ * Gating on `widget` would have been a guess about the container, and a wrong
687
+ * one in both directions — the wrapper setup scaffolds for a widget is
688
+ * full-width (the pane renders there), and a page-posture login on a phone is
689
+ * exactly the narrow case the advice is for. `hero` stays quiet: its compact
690
+ * fallback is editable text, so a narrow container never leaves it blank.
691
+ */
692
+ function designWarnings(design) {
693
+ if (design !== "split" && design !== "split-right") return [];
694
+ return [`The ${design} design shows its brand pane only when the login's container is wide; a narrow container — a card-width embed, or any phone-width viewport — collapses it to the compact brand mark. Set logo_url (or hero_url) in .zitadel/branding/branding.json so that mark isn't empty.`];
695
+ }
580
696
  //#endregion
581
697
  //#region src/commands/setup/index.ts
582
698
  /**
@@ -586,6 +702,16 @@ function fileNameOf(p) {
586
702
  * load.
587
703
  */
588
704
  const FRAMEWORK_OPTIONS = createOrca().availableFrameworks().map((framework) => framework.id);
705
+ /**
706
+ * `--renderer` offers only ids `getRenderer` will resolve: a
707
+ * declared-but-unpublished renderer (ADR 006) keeps its registry entry to
708
+ * reserve the id, but is surfaced as unavailable in the flag description
709
+ * instead of in `options`, so `--help` never advertises a value that is
710
+ * guaranteed to fail and an explicit pass is rejected at parse time — before
711
+ * any remote project is created.
712
+ */
713
+ const UNAVAILABLE_RENDERER_IDS = RENDERER_IDS.filter((id) => !AVAILABLE_RENDERER_IDS.includes(id));
714
+ const RENDERER_FLAG_DESCRIPTION = UNAVAILABLE_RENDERER_IDS.length === 0 ? "Renderer (default: react)." : `Renderer (default: react). Not yet available: ${UNAVAILABLE_RENDERER_IDS.join(", ")}.`;
589
715
  /** `zitadel setup` — create a project and scaffold local auth.
590
716
  *
591
717
  * Detects (or, for an empty directory, scaffolds then re-detects) the
@@ -607,14 +733,22 @@ var Setup = class Setup extends BaseCommand {
607
733
  options: FRAMEWORK_OPTIONS
608
734
  }),
609
735
  renderer: Flags.string({
610
- description: "Renderer (default: react).",
611
- options: [...RENDERER_IDS]
736
+ description: RENDERER_FLAG_DESCRIPTION,
737
+ options: [...AVAILABLE_RENDERER_IDS]
612
738
  }),
613
739
  "dev-port": Flags.integer({ description: "Dev-server port; also the issuer origin registered with Zitadel. Defaults to the detected port. Use distinct ports to run several scaffolded apps side by side." }),
614
740
  "skip-install": Flags.boolean({ description: "Do not install dependencies after setup updates package.json." }),
615
741
  preset: Flags.string({
616
742
  description: "Sign-in preset for the scaffolded schema and login flow (default: password-first).",
617
743
  options: [...SETUP_PRESETS]
744
+ }),
745
+ "use-case": Flags.string({
746
+ description: "Use case for the scaffolded schema fields: who signs in to the app (default: minimal).",
747
+ options: [...SETUP_USE_CASES]
748
+ }),
749
+ design: Flags.string({
750
+ description: "Login design to eject into .zitadel/branding/ and publish as branding revision 1. Skips the wizard's design question. When omitted in non-interactive runs, the login uses the built-in template; run the `branding eject` command later to customize. Split-family designs (split, split-right, hero) collapse their brand pane by container width: narrow containers — including widget-posture embeds at card width — render the compact brand mark instead (logo_url, else hero_url, from .zitadel/branding/branding.json; hero falls back to editable text).",
751
+ options: [...BRANDING_DESIGNS]
618
752
  })
619
753
  };
620
754
  async run() {
@@ -654,6 +788,8 @@ var Setup = class Setup extends BaseCommand {
654
788
  skip_install: Boolean(flags["skip-install"]),
655
789
  dev_port_explicit: flags["dev-port"] !== void 0,
656
790
  preset: flags.preset ?? DEFAULT_SETUP_PRESET,
791
+ use_case: flags["use-case"] ?? DEFAULT_SETUP_USE_CASE,
792
+ design: flags.design ?? "built-in",
657
793
  step: "framework_resolved"
658
794
  });
659
795
  if (flags["dev-port"] !== void 0) {
@@ -668,7 +804,9 @@ var Setup = class Setup extends BaseCommand {
668
804
  let answers = {
669
805
  server: this.meta.source,
670
806
  devPort: framework.devPort,
671
- preset: flags.preset ?? DEFAULT_SETUP_PRESET
807
+ preset: flags.preset ?? DEFAULT_SETUP_PRESET,
808
+ useCase: flags["use-case"] ?? DEFAULT_SETUP_USE_CASE,
809
+ design: flags.design
672
810
  };
673
811
  if (!nonInteractive && !dryRun) {
674
812
  intro("Zitadel setup");
@@ -677,12 +815,18 @@ var Setup = class Setup extends BaseCommand {
677
815
  cwd,
678
816
  serverFlag: this.meta.serverFlag,
679
817
  devPortFromFlag: flags["dev-port"] !== void 0,
680
- presetFromFlag: flags.preset !== void 0
818
+ presetFromFlag: flags.preset !== void 0,
819
+ useCaseFromFlag: flags["use-case"] !== void 0,
820
+ designFromFlag: flags.design !== void 0
681
821
  };
682
822
  for (const prompt of SETUP_PROMPTS) answers = await prompt.ask(answers, promptCtx);
683
823
  outro("Configuration captured");
684
824
  }
685
- this.recordTelemetry({ preset: answers.preset });
825
+ this.recordTelemetry({
826
+ preset: answers.preset,
827
+ use_case: answers.useCase,
828
+ design: answers.design ?? "built-in"
829
+ });
686
830
  const issuer = issuerFromPort(answers.devPort);
687
831
  framework = {
688
832
  ...framework,
@@ -696,10 +840,13 @@ var Setup = class Setup extends BaseCommand {
696
840
  ...retryOptionsFromFlags(flags),
697
841
  framework: framework.id,
698
842
  preset: answers.preset,
843
+ useCase: answers.useCase,
844
+ design: answers.design,
699
845
  devPort: answers.devPort
700
846
  });
701
847
  consola$1.success(`Created project ${project.id}`);
702
848
  this.recordTelemetry({ step: "project_created" });
849
+ const posture = derivePosture(framework.id, scaffoldedFramework);
703
850
  const ctx = {
704
851
  framework,
705
852
  rendererId: flags.renderer ?? "react",
@@ -708,7 +855,9 @@ var Setup = class Setup extends BaseCommand {
708
855
  server: answers.server,
709
856
  cliVersion: this.meta.cliVersion,
710
857
  scaffoldedFramework,
711
- preset: answers.preset
858
+ posture,
859
+ preset: answers.preset,
860
+ useCase: answers.useCase
712
861
  };
713
862
  consola$1.start(`Patching project files${dryRun ? " (dry run)" : ""}`);
714
863
  const result = await orca.patcherFor(framework.id).patch(ctx, {
@@ -717,21 +866,24 @@ var Setup = class Setup extends BaseCommand {
717
866
  force
718
867
  });
719
868
  for (const file of result.filesWritten) {
720
- const sentence = describeWrittenFile(relativeDisplay(cwd, file), dryRun);
869
+ const sentence = describeWrittenFile(relativeDisplayPath(cwd, file), dryRun);
721
870
  if (sentence) consola$1.info(sentence);
722
871
  }
723
- for (const file of result.filesSkipped) consola$1.info(`Left ${relativeDisplay(cwd, file)} unchanged (already matches target)`);
872
+ for (const file of result.filesSkipped) consola$1.info(`Left ${relativeDisplayPath(cwd, file)} unchanged (already matches target)`);
724
873
  let resourceResult;
725
874
  try {
726
875
  resourceResult = dryRun ? { filesWritten: [] } : await materializeSetupResources({
727
876
  cwd,
728
877
  client: createZitadelClient({
729
878
  baseUrl: answers.server,
730
- token: project.projectSecret
879
+ token: project.project_secret
731
880
  }),
732
881
  projectId: project.id,
733
882
  force,
734
- preset: answers.preset
883
+ preset: answers.preset,
884
+ useCase: answers.useCase,
885
+ design: answers.design,
886
+ cliVersion: this.meta.cliVersion
735
887
  });
736
888
  } catch (error) {
737
889
  await rm(join(cwd, "zitadel.json"), { force: true });
@@ -744,7 +896,7 @@ var Setup = class Setup extends BaseCommand {
744
896
  });
745
897
  }
746
898
  for (const file of resourceResult.filesWritten) {
747
- const sentence = describeWrittenFile(relativeDisplay(cwd, file), dryRun);
899
+ const sentence = describeWrittenFile(relativeDisplayPath(cwd, file), dryRun);
748
900
  if (sentence) consola$1.info(sentence);
749
901
  }
750
902
  const allFilesWritten = [...result.filesWritten, ...resourceResult.filesWritten];
@@ -753,6 +905,21 @@ var Setup = class Setup extends BaseCommand {
753
905
  step: "files_patched",
754
906
  files_written_count: allFilesWritten.length
755
907
  });
908
+ if (!dryRun) try {
909
+ await writeScaffoldManifest({
910
+ cwd,
911
+ actions: orca.patcherFor(framework.id).artifacts({
912
+ framework,
913
+ rendererId: ctx.rendererId
914
+ }),
915
+ written: [...result.filesWritten, ...result.filesSkipped],
916
+ scaffoldedFramework,
917
+ devPort: answers.devPort,
918
+ posture
919
+ });
920
+ } catch (error) {
921
+ consola$1.debug("Failed to record the scaffold manifest", error);
922
+ }
756
923
  const installOutcome = await installDependenciesForSetup({
757
924
  cliVersion: this.meta.cliVersion,
758
925
  cwd,
@@ -768,7 +935,18 @@ var Setup = class Setup extends BaseCommand {
768
935
  step: "dependencies_installed",
769
936
  package_manager: installOutcome.install.package_manager
770
937
  });
771
- const writtenRel = allFilesWritten.map((file) => relativeDisplay(cwd, file));
938
+ const writtenRel = allFilesWritten.map((file) => relativeDisplayPath(cwd, file));
939
+ const claimNudge = claimState({
940
+ secret: {},
941
+ server: answers.server
942
+ }).kind === "detached" ? {
943
+ actions: [claimAction(this.meta.cliVersion)],
944
+ commands: [claimCommand(this.meta.cliVersion)]
945
+ } : {
946
+ actions: [],
947
+ commands: []
948
+ };
949
+ const warnings = designWarnings(answers.design);
772
950
  if (!this.jsonEnabled()) {
773
951
  const sections = buildSummary({
774
952
  projectFacts: await detectProjectFacts(cwd, framework.id),
@@ -776,14 +954,15 @@ var Setup = class Setup extends BaseCommand {
776
954
  project,
777
955
  server: answers.server,
778
956
  issuer,
779
- scaffoldedFramework
957
+ scaffoldedFramework,
958
+ design: answers.design
780
959
  });
781
960
  consola$1.box({
782
961
  title: "Zitadel is ready",
783
962
  message: [
784
963
  renderSummary(sections),
785
964
  "",
786
- installOutcome.boxActions.join("\n")
965
+ [...installOutcome.boxActions, ...claimNudge.actions].join("\n")
787
966
  ].join("\n"),
788
967
  style: {
789
968
  padding: 1,
@@ -791,9 +970,11 @@ var Setup = class Setup extends BaseCommand {
791
970
  borderColor: "green"
792
971
  }
793
972
  });
973
+ for (const warning of warnings) consola$1.warn(warning);
794
974
  }
795
975
  return this.emit({
796
976
  status: "ok",
977
+ warnings,
797
978
  pretty: "",
798
979
  data: {
799
980
  title: "Zitadel is ready.",
@@ -803,11 +984,21 @@ var Setup = class Setup extends BaseCommand {
803
984
  },
804
985
  framework: framework.id,
805
986
  server: answers.server,
806
- files_written: allFilesWritten.map((file) => relativeDisplay(cwd, file)),
807
- files_skipped: result.filesSkipped.map((file) => relativeDisplay(cwd, file)),
987
+ files_written: allFilesWritten.map((file) => relativeDisplayPath(cwd, file)),
988
+ files: result.files.map((file) => ({
989
+ path: relativeDisplayPath(cwd, file.path),
990
+ kind: file.kind,
991
+ action: file.action
992
+ })),
993
+ files_skipped: result.filesSkipped.map((file) => relativeDisplayPath(cwd, file)),
808
994
  install: installOutcome.install,
809
- next_actions: installOutcome.nextActions,
810
- next_commands: installOutcome.nextCommands
995
+ design: answers.design ?? null,
996
+ next_actions: [
997
+ ...installOutcome.nextActions,
998
+ brandingGuidanceAction(answers.design, this.meta.cliVersion),
999
+ ...claimNudge.actions
1000
+ ],
1001
+ next_commands: [...installOutcome.nextCommands, ...claimNudge.commands]
811
1002
  }
812
1003
  });
813
1004
  }
@@ -836,10 +1027,10 @@ async function resolveScaffoldFramework(framework, nonInteractive, orca) {
836
1027
  function dryRunProject(issuer) {
837
1028
  return {
838
1029
  id: "dry-run-0000",
839
- projectSecret: "sk_proj_dry_run_full",
840
- previewSecret: "sk_proj_dry_run_preview",
841
- previewOrigins: [issuer],
842
- createdAt: "2026-04-21T14:03:11.000Z"
1030
+ project_secret: "sk_proj_dry_run_full",
1031
+ preview_secret: "sk_proj_dry_run_preview",
1032
+ preview_origins: [issuer],
1033
+ created_at: "2026-04-21T14:03:11.000Z"
843
1034
  };
844
1035
  }
845
1036
  /**
@@ -851,6 +1042,8 @@ function setupRetryFlags(opts) {
851
1042
  const parts = [];
852
1043
  if (opts.framework) parts.push(`--framework ${opts.framework}`);
853
1044
  if (opts.preset && opts.preset !== DEFAULT_SETUP_PRESET) parts.push(`--preset ${opts.preset}`);
1045
+ if (opts.useCase && opts.useCase !== DEFAULT_SETUP_USE_CASE) parts.push(`--use-case ${opts.useCase}`);
1046
+ if (opts.design) parts.push(`--design ${opts.design}`);
854
1047
  if (opts.renderer && opts.renderer !== "react") parts.push(`--renderer ${opts.renderer}`);
855
1048
  if (opts.devPort !== void 0) parts.push(`--dev-port ${opts.devPort}`);
856
1049
  if (opts.nonInteractive) parts.push("--non-interactive");
@@ -866,6 +1059,8 @@ function retryOptionsFromFlags(flags) {
866
1059
  return {
867
1060
  framework: flags.framework,
868
1061
  preset: flags.preset,
1062
+ useCase: flags["use-case"],
1063
+ design: flags.design,
869
1064
  renderer: flags.renderer,
870
1065
  devPort: flags["dev-port"],
871
1066
  nonInteractive: Boolean(flags["non-interactive"])
@@ -875,8 +1070,8 @@ async function createProjectWithLocalHint(client, server, cliVersion, projectNam
875
1070
  try {
876
1071
  const payload = {
877
1072
  name: projectName,
878
- previewOrigins: [issuer],
879
- seedDefaults: false
1073
+ preview_origins: [issuer],
1074
+ seed_defaults: false
880
1075
  };
881
1076
  return await client.createProject(payload);
882
1077
  } catch (error) {
@@ -906,10 +1101,6 @@ function localSetupHint(error, retry, cliVersion) {
906
1101
  details: normalized.details
907
1102
  });
908
1103
  }
909
- /** Renders an absolute path relative to `cwd` for human-readable output. */
910
- function relativeDisplay(cwd, path) {
911
- return path.startsWith(cwd) ? path.slice(cwd.length + 1) : path;
912
- }
913
1104
  /**
914
1105
  * Replaces the user's `$HOME` with `~` in a path for compact terminal output.
915
1106
  * Falls back to the raw path when `HOME` isn't set or doesn't match.
@@ -930,15 +1121,13 @@ function pickWrittenFile(written, suffix) {
930
1121
  }
931
1122
  /**
932
1123
  * Translates a patcher-written path into a single sentence the user can
933
- * read at narration speed. Returns `null` for directories and other
934
- * scaffolding artefacts that aren't worth narrating individually the
935
- * file count in the closing `success(...)` and the summary's INSTALLED
936
- * section already cover them. The verb tense flips for `--dry-run` so
937
- * the user sees a preview ("Would write ...") instead of a claim that
938
- * something happened.
1124
+ * read at narration speed. The patch result's `filesWritten` carries
1125
+ * deduplicated file paths only (directories stay in the typed `files`
1126
+ * rows), so no artefact filtering is needed here. The verb tense flips
1127
+ * for `--dry-run` so the user sees a preview ("Would write ...") instead
1128
+ * of a claim that something happened.
939
1129
  */
940
1130
  function describeWrittenFile(relPath, dryRun) {
941
- if (relPath === ".zitadel" || relPath === ".zitadel/flows" || relPath === ".zitadel/schemas" || relPath === ".zitadel/meta") return null;
942
1131
  const verb = dryRun ? "Would write" : "Wrote";
943
1132
  const sentence = SENTENCE_BY_PATH[relPath];
944
1133
  if (sentence) return `${verb} ${sentence.subject} (${path(relPath)})`;
@@ -964,6 +1153,10 @@ const SENTENCE_BY_PATH = {
964
1153
  ".zitadel/meta/flow-definition.json": { subject: "the flow dialect spec (editor $schema)" },
965
1154
  ".zitadel/meta/user-schema.json": { subject: "the user-schema dialect spec" },
966
1155
  ".zitadel/meta/user-property.json": { subject: "the user-property dialect spec" },
1156
+ ".zitadel/meta/branding.json": { subject: "the branding dialect spec" },
1157
+ ".zitadel/branding/branding.json": { subject: "the branding descriptor (layout + asset URLs)" },
1158
+ ".zitadel/branding/login.liquid": { subject: "the editable login template" },
1159
+ ".zitadel/branding/README.md": { subject: "the branding folder README" },
967
1160
  "AGENTS.md": { subject: "the agent guidance (golden journey + config dialect)" },
968
1161
  "README.md": { subject: "the README's Zitadel section" },
969
1162
  "app/page.tsx": { subject: "the home page redirect" },
@@ -977,7 +1170,7 @@ const SENTENCE_BY_PATH = {
977
1170
  };
978
1171
  /** Builds the section list driving {@link renderSummary} for the setup command. */
979
1172
  function buildSummary(opts) {
980
- const { projectFacts, writtenRel, project, server, issuer, scaffoldedFramework } = opts;
1173
+ const { projectFacts, writtenRel, project, server, issuer, scaffoldedFramework, design } = opts;
981
1174
  const sdkPackage = "@zitadel/sdk-next";
982
1175
  const packageJsonHit = pickWrittenFile(writtenRel, "package.json");
983
1176
  const detected = [{
@@ -1009,6 +1202,28 @@ function buildSummary(opts) {
1009
1202
  value: path(hit)
1010
1203
  });
1011
1204
  }
1205
+ const customizeRows = [];
1206
+ for (const [label, suffix, dir] of [
1207
+ [
1208
+ "User schema",
1209
+ ".zitadel/schemas/default-human-user.json",
1210
+ ".zitadel/schemas/"
1211
+ ],
1212
+ [
1213
+ "Login flow",
1214
+ ".zitadel/flows/default-login.json",
1215
+ ".zitadel/flows/"
1216
+ ],
1217
+ [
1218
+ "Login template",
1219
+ ".zitadel/branding/login.liquid",
1220
+ ".zitadel/branding/"
1221
+ ]
1222
+ ]) if (pickWrittenFile(writtenRel, suffix)) customizeRows.push({
1223
+ label,
1224
+ value: path(dir),
1225
+ secondary: "see its README.md"
1226
+ });
1012
1227
  const projectRows = [
1013
1228
  {
1014
1229
  label: "Project id",
@@ -1021,6 +1236,14 @@ function buildSummary(opts) {
1021
1236
  {
1022
1237
  label: "App will run",
1023
1238
  value: url(issuer)
1239
+ },
1240
+ design ? {
1241
+ label: "Login design",
1242
+ value: brandingDesignLabel(design),
1243
+ secondary: `${design} · ${path(".zitadel/branding/")}`
1244
+ } : {
1245
+ label: "Login design",
1246
+ value: dim("built-in template")
1024
1247
  }
1025
1248
  ];
1026
1249
  return [
@@ -1032,6 +1255,10 @@ function buildSummary(opts) {
1032
1255
  title: "Installed",
1033
1256
  rows: installedRows
1034
1257
  },
1258
+ {
1259
+ title: "Customize",
1260
+ rows: customizeRows
1261
+ },
1035
1262
  {
1036
1263
  title: "Project",
1037
1264
  rows: projectRows