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

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 (58) hide show
  1. package/README.md +145 -107
  2. package/SKILLS.md +88 -12
  3. package/dist/branding-BXj028-W.mjs +79 -0
  4. package/dist/branding-BXj028-W.mjs.map +1 -0
  5. package/dist/claim-state-BTGInAaN.mjs +66 -0
  6. package/dist/claim-state-BTGInAaN.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 +125 -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 +477 -37
  14. package/dist/commands/doctor.mjs.map +1 -1
  15. package/dist/commands/eject.mjs +3 -3
  16. package/dist/commands/logs.mjs +2 -2
  17. package/dist/commands/plan.mjs +6 -5
  18. package/dist/commands/plan.mjs.map +1 -1
  19. package/dist/commands/reset.mjs +2 -2
  20. package/dist/commands/schemas/list.mjs +8 -8
  21. package/dist/commands/schemas/list.mjs.map +1 -1
  22. package/dist/commands/setup.mjs +305 -36
  23. package/dist/commands/setup.mjs.map +1 -1
  24. package/dist/commands/start.mjs +3 -3
  25. package/dist/commands/status.mjs +21 -13
  26. package/dist/commands/status.mjs.map +1 -1
  27. package/dist/commands/stop.mjs +3 -3
  28. package/dist/commands/stop.mjs.map +1 -1
  29. package/dist/{docker-pGcTrDIY.mjs → docker-Ciz9N6uI.mjs} +2 -2
  30. package/dist/{docker-pGcTrDIY.mjs.map → docker-Ciz9N6uI.mjs.map} +1 -1
  31. package/dist/{environment-BQF7LeCz.mjs → environment-CuHKxtIO.mjs} +1 -1
  32. package/dist/{environment-BQF7LeCz.mjs.map → environment-CuHKxtIO.mjs.map} +1 -1
  33. package/dist/{journey-guidance-CYVpQszz.mjs → journey-guidance-CtQUfWar.mjs} +14 -3
  34. package/dist/{journey-guidance-CYVpQszz.mjs.map → journey-guidance-CtQUfWar.mjs.map} +1 -1
  35. package/dist/{oclif-BRdjVNne.mjs → oclif-CL6-M13m.mjs} +201 -43
  36. package/dist/oclif-CL6-M13m.mjs.map +1 -0
  37. package/dist/{orca-Dip6DPJ2.mjs → orca-BWB4zOdw.mjs} +749 -192
  38. package/dist/orca-BWB4zOdw.mjs.map +1 -0
  39. package/dist/{ports-V8JfiNgT.mjs → ports-Cu2Y7aXQ.mjs} +2 -2
  40. package/dist/{ports-V8JfiNgT.mjs.map → ports-Cu2Y7aXQ.mjs.map} +1 -1
  41. package/dist/{processes-BVqYsxT8.mjs → processes-Bn2WngOi.mjs} +1 -1
  42. package/dist/{processes-BVqYsxT8.mjs.map → processes-Bn2WngOi.mjs.map} +1 -1
  43. package/dist/{project-BGgv7Bba.mjs → project-yejJSfdJ.mjs} +57 -4
  44. package/dist/project-yejJSfdJ.mjs.map +1 -0
  45. package/dist/scaffold-manifest-CKNFLX9T.mjs +81 -0
  46. package/dist/scaffold-manifest-CKNFLX9T.mjs.map +1 -0
  47. package/dist/{sync-Cd3z7Q1R.mjs → sync-Cx2_4rXW.mjs} +99 -50
  48. package/dist/sync-Cx2_4rXW.mjs.map +1 -0
  49. package/dist/user-schema-W1OsXdpc.mjs +91 -0
  50. package/dist/user-schema-W1OsXdpc.mjs.map +1 -0
  51. package/oclif.manifest.json +227 -5
  52. package/package.json +8 -6
  53. package/dist/oclif-BRdjVNne.mjs.map +0 -1
  54. package/dist/orca-Dip6DPJ2.mjs.map +0 -1
  55. package/dist/project-BGgv7Bba.mjs.map +0 -1
  56. package/dist/sync-Cd3z7Q1R.mjs.map +0 -1
  57. package/dist/user-schema-DDz5-lX5.mjs +0 -13
  58. 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 { D as toZitadelError, E as ZitadelError, T as stableStringify, k as publicCliCommand, n as DEFAULT_SERVER, t as BaseCommand, u as detectHealthyLocalServer } from "../oclif-CL6-M13m.mjs";
2
+ import { n as claimCommand, r as claimState, t as claimAction } from "../claim-state-BTGInAaN.mjs";
3
+ import { n as customizeAndPublishActions, r as verifyLoginAction, t as brandingGuidanceAction } from "../journey-guidance-CtQUfWar.mjs";
4
+ import { i as RENDERER_IDS, n as inspectScaffoldTarget, r as AVAILABLE_RENDERER_IDS, s as issuerFromPort, t as createOrca } from "../orca-BWB4zOdw.mjs";
5
+ import { n as hasZitadelSecret, t as hasZitadelConfig } from "../project-yejJSfdJ.mjs";
6
+ import { a as updateState, t as SCHEMAS_DIR } from "../user-schema-W1OsXdpc.mjs";
7
+ import { r as writeScaffoldManifest } from "../scaffold-manifest-CKNFLX9T.mjs";
8
+ import { i as toBrandingWireBody, t as BRANDING_DIR } from "../branding-BXj028-W.mjs";
9
+ import { c as writeBackResource, o as hashForState, u as FLOWS_DIR } from "../sync-Cx2_4rXW.mjs";
7
10
  import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
8
11
  import { basename, dirname, join } from "node:path";
9
12
  import { cancel, confirm, intro, isCancel, outro, select, spinner, text } from "@clack/prompts";
10
13
  import { Flags } from "@oclif/core";
11
14
  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";
15
+ 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
16
  import { consola as consola$1 } from "consola";
17
+ import { BRANDING_FILE_SCHEMA_REF } from "@zitadel/config/meta-schemas";
14
18
  import { spawn } from "node:child_process";
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,7 +63,11 @@ 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);
@@ -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,6 +103,24 @@ 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, brandingReadmeContent())) 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
126
  if (await writeReadmeFile(opts.cwd, schemasReadme, schemasReadmeContent())) filesWritten.push(join(opts.cwd, schemasReadme));
@@ -73,6 +128,22 @@ async function materializeSetupResources(opts) {
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.
@@ -294,6 +365,77 @@ function bail(value) {
294
365
  }
295
366
  }
296
367
  //#endregion
368
+ //#region src/commands/setup/prompts/design.ts
369
+ /**
370
+ * Sentinel select value for "keep the built-in login" — clack option values
371
+ * must be strings, and `undefined` in {@link SetupAnswers.design} is the
372
+ * real representation of that choice.
373
+ */
374
+ const BUILT_IN = "built-in";
375
+ /**
376
+ * "How should the login look?" — the last wizard question (#676). Asks after
377
+ * the sign-in preset so the order tells a story: the use case owns what is
378
+ * collected, the preset owns how users authenticate, the design owns how
379
+ * that experience looks.
380
+ *
381
+ * The built-in default is preselected and writes nothing. Picking a starter
382
+ * design forks its Liquid template into `.zitadel/branding/` and publishes
383
+ * it as branding revision 1 — from then on the template is repo-owned and
384
+ * stops tracking built-in improvements, so the option labels make the
385
+ * file-write explicit and keep ownership an opt-in. `--design` is
386
+ * authoritative; non-interactive runs keep the built-in template.
387
+ */
388
+ var DesignPrompt = class {
389
+ async ask(answers, ctx) {
390
+ if (ctx.designFromFlag) return answers;
391
+ const value = await select({
392
+ message: "How should the login look? A starter design adds its editable template to .zitadel/branding/.",
393
+ initialValue: answers.design ?? BUILT_IN,
394
+ options: [
395
+ {
396
+ value: BUILT_IN,
397
+ label: "Built-in",
398
+ hint: "no files added; pick a design anytime later with `branding eject`"
399
+ },
400
+ {
401
+ value: "centered",
402
+ label: "Centered",
403
+ hint: "card centred on the page"
404
+ },
405
+ {
406
+ value: "split",
407
+ label: "Split",
408
+ hint: "brand panel left, form right"
409
+ },
410
+ {
411
+ value: "split-right",
412
+ label: "Split (reversed)",
413
+ hint: "form left, brand panel right"
414
+ },
415
+ {
416
+ value: "hero",
417
+ label: "Hero",
418
+ hint: "landing-style brand pane left, form right"
419
+ },
420
+ {
421
+ value: "minimal",
422
+ label: "Minimal",
423
+ hint: "no card chrome, fields straight on the page"
424
+ }
425
+ ]
426
+ });
427
+ bail(value);
428
+ if (value === BUILT_IN) return {
429
+ ...answers,
430
+ design: void 0
431
+ };
432
+ return {
433
+ ...answers,
434
+ design: value
435
+ };
436
+ }
437
+ };
438
+ //#endregion
297
439
  //#region src/commands/setup/prompts/dev-port.ts
298
440
  /**
299
441
  * "Dev server port" — defaults to the detected port. The validated answer
@@ -445,6 +587,48 @@ var SignInPresetPrompt = class {
445
587
  }
446
588
  };
447
589
  //#endregion
590
+ //#region src/commands/setup/prompts/use-case.ts
591
+ /**
592
+ * "Who will sign in to your app?" — picks the use case, which owns the
593
+ * schema field set the scaffold collects (#448). Orthogonal to the sign-in
594
+ * preset, so this asks first: what a user *is* before *how* they sign in.
595
+ * `--use-case` is authoritative; non-interactive runs keep the minimal
596
+ * default seeded by the command.
597
+ */
598
+ var UseCasePrompt = class {
599
+ async ask(answers, ctx) {
600
+ if (ctx.useCaseFromFlag) return answers;
601
+ const options = {
602
+ minimal: {
603
+ label: "Just me or a small group",
604
+ hint: "collects email"
605
+ },
606
+ consumer: {
607
+ label: "Consumers",
608
+ hint: "collects email, given & family name"
609
+ },
610
+ business: {
611
+ label: "Business",
612
+ hint: "collects email, given & family name, company"
613
+ }
614
+ };
615
+ const value = await select({
616
+ message: "Who will sign in to your app? Collected fields stay editable after setup.",
617
+ initialValue: answers.useCase,
618
+ options: SETUP_USE_CASES.map((useCase) => ({
619
+ value: useCase,
620
+ label: options[useCase].label,
621
+ hint: options[useCase].hint
622
+ }))
623
+ });
624
+ bail(value);
625
+ return {
626
+ ...answers,
627
+ useCase: value
628
+ };
629
+ }
630
+ };
631
+ //#endregion
448
632
  //#region src/commands/setup/prompts/pick-framework.ts
449
633
  /**
450
634
  * "Choose a framework to scaffold" — the only prompt outside the main wizard.
@@ -484,7 +668,9 @@ const SETUP_PROMPTS = [
484
668
  new FrameworkConfirmPrompt(),
485
669
  new ServerPrompt(),
486
670
  new DevPortPrompt(),
487
- new SignInPresetPrompt()
671
+ new UseCasePrompt(),
672
+ new SignInPresetPrompt(),
673
+ new DesignPrompt()
488
674
  ];
489
675
  //#endregion
490
676
  //#region src/commands/setup/summary.ts
@@ -517,6 +703,8 @@ const path = (s) => pc.cyan(s);
517
703
  const url = (s) => pc.cyan(s);
518
704
  /** Yellow, for opaque ids the user shouldn't try to read. */
519
705
  const id = (s) => pc.yellow(s);
706
+ /** Dim, for separators and secondary detail. */
707
+ const dim = (s) => pc.dim(s);
520
708
  /**
521
709
  * Reads the project root to identify the framework version, TS presence,
522
710
  * and which package manager the user runs. Returns safe defaults (npm
@@ -586,6 +774,16 @@ function fileNameOf(p) {
586
774
  * load.
587
775
  */
588
776
  const FRAMEWORK_OPTIONS = createOrca().availableFrameworks().map((framework) => framework.id);
777
+ /**
778
+ * `--renderer` offers only ids `getRenderer` will resolve: a
779
+ * declared-but-unpublished renderer (ADR 006) keeps its registry entry to
780
+ * reserve the id, but is surfaced as unavailable in the flag description
781
+ * instead of in `options`, so `--help` never advertises a value that is
782
+ * guaranteed to fail and an explicit pass is rejected at parse time — before
783
+ * any remote project is created.
784
+ */
785
+ const UNAVAILABLE_RENDERER_IDS = RENDERER_IDS.filter((id) => !AVAILABLE_RENDERER_IDS.includes(id));
786
+ const RENDERER_FLAG_DESCRIPTION = UNAVAILABLE_RENDERER_IDS.length === 0 ? "Renderer (default: react)." : `Renderer (default: react). Not yet available: ${UNAVAILABLE_RENDERER_IDS.join(", ")}.`;
589
787
  /** `zitadel setup` — create a project and scaffold local auth.
590
788
  *
591
789
  * Detects (or, for an empty directory, scaffolds then re-detects) the
@@ -607,14 +805,22 @@ var Setup = class Setup extends BaseCommand {
607
805
  options: FRAMEWORK_OPTIONS
608
806
  }),
609
807
  renderer: Flags.string({
610
- description: "Renderer (default: react).",
611
- options: [...RENDERER_IDS]
808
+ description: RENDERER_FLAG_DESCRIPTION,
809
+ options: [...AVAILABLE_RENDERER_IDS]
612
810
  }),
613
811
  "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
812
  "skip-install": Flags.boolean({ description: "Do not install dependencies after setup updates package.json." }),
615
813
  preset: Flags.string({
616
814
  description: "Sign-in preset for the scaffolded schema and login flow (default: password-first).",
617
815
  options: [...SETUP_PRESETS]
816
+ }),
817
+ "use-case": Flags.string({
818
+ description: "Use case for the scaffolded schema fields: who signs in to the app (default: minimal).",
819
+ options: [...SETUP_USE_CASES]
820
+ }),
821
+ design: Flags.string({
822
+ 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.",
823
+ options: [...BRANDING_DESIGNS]
618
824
  })
619
825
  };
620
826
  async run() {
@@ -654,6 +860,8 @@ var Setup = class Setup extends BaseCommand {
654
860
  skip_install: Boolean(flags["skip-install"]),
655
861
  dev_port_explicit: flags["dev-port"] !== void 0,
656
862
  preset: flags.preset ?? DEFAULT_SETUP_PRESET,
863
+ use_case: flags["use-case"] ?? DEFAULT_SETUP_USE_CASE,
864
+ design: flags.design ?? "built-in",
657
865
  step: "framework_resolved"
658
866
  });
659
867
  if (flags["dev-port"] !== void 0) {
@@ -668,7 +876,9 @@ var Setup = class Setup extends BaseCommand {
668
876
  let answers = {
669
877
  server: this.meta.source,
670
878
  devPort: framework.devPort,
671
- preset: flags.preset ?? DEFAULT_SETUP_PRESET
879
+ preset: flags.preset ?? DEFAULT_SETUP_PRESET,
880
+ useCase: flags["use-case"] ?? DEFAULT_SETUP_USE_CASE,
881
+ design: flags.design
672
882
  };
673
883
  if (!nonInteractive && !dryRun) {
674
884
  intro("Zitadel setup");
@@ -677,12 +887,18 @@ var Setup = class Setup extends BaseCommand {
677
887
  cwd,
678
888
  serverFlag: this.meta.serverFlag,
679
889
  devPortFromFlag: flags["dev-port"] !== void 0,
680
- presetFromFlag: flags.preset !== void 0
890
+ presetFromFlag: flags.preset !== void 0,
891
+ useCaseFromFlag: flags["use-case"] !== void 0,
892
+ designFromFlag: flags.design !== void 0
681
893
  };
682
894
  for (const prompt of SETUP_PROMPTS) answers = await prompt.ask(answers, promptCtx);
683
895
  outro("Configuration captured");
684
896
  }
685
- this.recordTelemetry({ preset: answers.preset });
897
+ this.recordTelemetry({
898
+ preset: answers.preset,
899
+ use_case: answers.useCase,
900
+ design: answers.design ?? "built-in"
901
+ });
686
902
  const issuer = issuerFromPort(answers.devPort);
687
903
  framework = {
688
904
  ...framework,
@@ -696,10 +912,13 @@ var Setup = class Setup extends BaseCommand {
696
912
  ...retryOptionsFromFlags(flags),
697
913
  framework: framework.id,
698
914
  preset: answers.preset,
915
+ useCase: answers.useCase,
916
+ design: answers.design,
699
917
  devPort: answers.devPort
700
918
  });
701
919
  consola$1.success(`Created project ${project.id}`);
702
920
  this.recordTelemetry({ step: "project_created" });
921
+ const posture = derivePosture(framework.id, scaffoldedFramework);
703
922
  const ctx = {
704
923
  framework,
705
924
  rendererId: flags.renderer ?? "react",
@@ -708,7 +927,9 @@ var Setup = class Setup extends BaseCommand {
708
927
  server: answers.server,
709
928
  cliVersion: this.meta.cliVersion,
710
929
  scaffoldedFramework,
711
- preset: answers.preset
930
+ posture,
931
+ preset: answers.preset,
932
+ useCase: answers.useCase
712
933
  };
713
934
  consola$1.start(`Patching project files${dryRun ? " (dry run)" : ""}`);
714
935
  const result = await orca.patcherFor(framework.id).patch(ctx, {
@@ -727,11 +948,13 @@ var Setup = class Setup extends BaseCommand {
727
948
  cwd,
728
949
  client: createZitadelClient({
729
950
  baseUrl: answers.server,
730
- token: project.projectSecret
951
+ token: project.project_secret
731
952
  }),
732
953
  projectId: project.id,
733
954
  force,
734
- preset: answers.preset
955
+ preset: answers.preset,
956
+ useCase: answers.useCase,
957
+ design: answers.design
735
958
  });
736
959
  } catch (error) {
737
960
  await rm(join(cwd, "zitadel.json"), { force: true });
@@ -753,6 +976,21 @@ var Setup = class Setup extends BaseCommand {
753
976
  step: "files_patched",
754
977
  files_written_count: allFilesWritten.length
755
978
  });
979
+ if (!dryRun) try {
980
+ await writeScaffoldManifest({
981
+ cwd,
982
+ actions: orca.patcherFor(framework.id).artifacts({
983
+ framework,
984
+ rendererId: ctx.rendererId
985
+ }),
986
+ written: [...result.filesWritten, ...result.filesSkipped],
987
+ scaffoldedFramework,
988
+ devPort: answers.devPort,
989
+ posture
990
+ });
991
+ } catch (error) {
992
+ consola$1.debug("Failed to record the scaffold manifest", error);
993
+ }
756
994
  const installOutcome = await installDependenciesForSetup({
757
995
  cliVersion: this.meta.cliVersion,
758
996
  cwd,
@@ -769,6 +1007,16 @@ var Setup = class Setup extends BaseCommand {
769
1007
  package_manager: installOutcome.install.package_manager
770
1008
  });
771
1009
  const writtenRel = allFilesWritten.map((file) => relativeDisplay(cwd, file));
1010
+ const claimNudge = claimState({
1011
+ secret: {},
1012
+ server: answers.server
1013
+ }).kind === "detached" ? {
1014
+ actions: [claimAction(this.meta.cliVersion)],
1015
+ commands: [claimCommand(this.meta.cliVersion)]
1016
+ } : {
1017
+ actions: [],
1018
+ commands: []
1019
+ };
772
1020
  if (!this.jsonEnabled()) {
773
1021
  const sections = buildSummary({
774
1022
  projectFacts: await detectProjectFacts(cwd, framework.id),
@@ -776,14 +1024,15 @@ var Setup = class Setup extends BaseCommand {
776
1024
  project,
777
1025
  server: answers.server,
778
1026
  issuer,
779
- scaffoldedFramework
1027
+ scaffoldedFramework,
1028
+ design: answers.design
780
1029
  });
781
1030
  consola$1.box({
782
1031
  title: "Zitadel is ready",
783
1032
  message: [
784
1033
  renderSummary(sections),
785
1034
  "",
786
- installOutcome.boxActions.join("\n")
1035
+ [...installOutcome.boxActions, ...claimNudge.actions].join("\n")
787
1036
  ].join("\n"),
788
1037
  style: {
789
1038
  padding: 1,
@@ -804,10 +1053,20 @@ var Setup = class Setup extends BaseCommand {
804
1053
  framework: framework.id,
805
1054
  server: answers.server,
806
1055
  files_written: allFilesWritten.map((file) => relativeDisplay(cwd, file)),
1056
+ files: result.files.map((file) => ({
1057
+ path: relativeDisplay(cwd, file.path),
1058
+ kind: file.kind,
1059
+ action: file.action
1060
+ })),
807
1061
  files_skipped: result.filesSkipped.map((file) => relativeDisplay(cwd, file)),
808
1062
  install: installOutcome.install,
809
- next_actions: installOutcome.nextActions,
810
- next_commands: installOutcome.nextCommands
1063
+ design: answers.design ?? null,
1064
+ next_actions: [
1065
+ ...installOutcome.nextActions,
1066
+ brandingGuidanceAction(answers.design, this.meta.cliVersion),
1067
+ ...claimNudge.actions
1068
+ ],
1069
+ next_commands: [...installOutcome.nextCommands, ...claimNudge.commands]
811
1070
  }
812
1071
  });
813
1072
  }
@@ -836,10 +1095,10 @@ async function resolveScaffoldFramework(framework, nonInteractive, orca) {
836
1095
  function dryRunProject(issuer) {
837
1096
  return {
838
1097
  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"
1098
+ project_secret: "sk_proj_dry_run_full",
1099
+ preview_secret: "sk_proj_dry_run_preview",
1100
+ preview_origins: [issuer],
1101
+ created_at: "2026-04-21T14:03:11.000Z"
843
1102
  };
844
1103
  }
845
1104
  /**
@@ -851,6 +1110,8 @@ function setupRetryFlags(opts) {
851
1110
  const parts = [];
852
1111
  if (opts.framework) parts.push(`--framework ${opts.framework}`);
853
1112
  if (opts.preset && opts.preset !== DEFAULT_SETUP_PRESET) parts.push(`--preset ${opts.preset}`);
1113
+ if (opts.useCase && opts.useCase !== DEFAULT_SETUP_USE_CASE) parts.push(`--use-case ${opts.useCase}`);
1114
+ if (opts.design) parts.push(`--design ${opts.design}`);
854
1115
  if (opts.renderer && opts.renderer !== "react") parts.push(`--renderer ${opts.renderer}`);
855
1116
  if (opts.devPort !== void 0) parts.push(`--dev-port ${opts.devPort}`);
856
1117
  if (opts.nonInteractive) parts.push("--non-interactive");
@@ -866,6 +1127,8 @@ function retryOptionsFromFlags(flags) {
866
1127
  return {
867
1128
  framework: flags.framework,
868
1129
  preset: flags.preset,
1130
+ useCase: flags["use-case"],
1131
+ design: flags.design,
869
1132
  renderer: flags.renderer,
870
1133
  devPort: flags["dev-port"],
871
1134
  nonInteractive: Boolean(flags["non-interactive"])
@@ -875,8 +1138,8 @@ async function createProjectWithLocalHint(client, server, cliVersion, projectNam
875
1138
  try {
876
1139
  const payload = {
877
1140
  name: projectName,
878
- previewOrigins: [issuer],
879
- seedDefaults: false
1141
+ preview_origins: [issuer],
1142
+ seed_defaults: false
880
1143
  };
881
1144
  return await client.createProject(payload);
882
1145
  } catch (error) {
@@ -930,15 +1193,13 @@ function pickWrittenFile(written, suffix) {
930
1193
  }
931
1194
  /**
932
1195
  * 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.
1196
+ * read at narration speed. The patch result's `filesWritten` carries
1197
+ * deduplicated file paths only (directories stay in the typed `files`
1198
+ * rows), so no artefact filtering is needed here. The verb tense flips
1199
+ * for `--dry-run` so the user sees a preview ("Would write ...") instead
1200
+ * of a claim that something happened.
939
1201
  */
940
1202
  function describeWrittenFile(relPath, dryRun) {
941
- if (relPath === ".zitadel" || relPath === ".zitadel/flows" || relPath === ".zitadel/schemas" || relPath === ".zitadel/meta") return null;
942
1203
  const verb = dryRun ? "Would write" : "Wrote";
943
1204
  const sentence = SENTENCE_BY_PATH[relPath];
944
1205
  if (sentence) return `${verb} ${sentence.subject} (${path(relPath)})`;
@@ -977,7 +1238,7 @@ const SENTENCE_BY_PATH = {
977
1238
  };
978
1239
  /** Builds the section list driving {@link renderSummary} for the setup command. */
979
1240
  function buildSummary(opts) {
980
- const { projectFacts, writtenRel, project, server, issuer, scaffoldedFramework } = opts;
1241
+ const { projectFacts, writtenRel, project, server, issuer, scaffoldedFramework, design } = opts;
981
1242
  const sdkPackage = "@zitadel/sdk-next";
982
1243
  const packageJsonHit = pickWrittenFile(writtenRel, "package.json");
983
1244
  const detected = [{
@@ -1021,6 +1282,14 @@ function buildSummary(opts) {
1021
1282
  {
1022
1283
  label: "App will run",
1023
1284
  value: url(issuer)
1285
+ },
1286
+ design ? {
1287
+ label: "Login design",
1288
+ value: design,
1289
+ secondary: path(".zitadel/branding/")
1290
+ } : {
1291
+ label: "Login design",
1292
+ value: dim("built-in template")
1024
1293
  }
1025
1294
  ];
1026
1295
  return [