@zitadel/cli 0.1.0-alpha.16 → 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 (62) hide show
  1. package/README.md +145 -107
  2. package/SKILLS.md +98 -13
  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 +12 -7
  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 -38
  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 +7 -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 +14 -10
  21. package/dist/commands/schemas/list.mjs.map +1 -1
  22. package/dist/commands/setup.mjs +331 -120
  23. package/dist/commands/setup.mjs.map +1 -1
  24. package/dist/commands/start.mjs +3 -4
  25. package/dist/commands/start.mjs.map +1 -1
  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/{docker-BbX0yOpg.mjs → docker-Ciz9N6uI.mjs} +2 -2
  31. package/dist/{docker-BbX0yOpg.mjs.map → docker-Ciz9N6uI.mjs.map} +1 -1
  32. package/dist/{environment-ABj1IWZt.mjs → environment-CuHKxtIO.mjs} +1 -1
  33. package/dist/{environment-ABj1IWZt.mjs.map → environment-CuHKxtIO.mjs.map} +1 -1
  34. package/dist/{journey-guidance-E_3_UwGt.mjs → journey-guidance-CtQUfWar.mjs} +14 -3
  35. package/dist/{journey-guidance-E_3_UwGt.mjs.map → journey-guidance-CtQUfWar.mjs.map} +1 -1
  36. package/dist/{oclif-SjHLB_XK.mjs → oclif-CL6-M13m.mjs} +220 -43
  37. package/dist/oclif-CL6-M13m.mjs.map +1 -0
  38. package/dist/{orca-_TmrJQMw.mjs → orca-BWB4zOdw.mjs} +749 -192
  39. package/dist/orca-BWB4zOdw.mjs.map +1 -0
  40. package/dist/{ports-XujDtA47.mjs → ports-Cu2Y7aXQ.mjs} +25 -20
  41. package/dist/ports-Cu2Y7aXQ.mjs.map +1 -0
  42. package/dist/{processes-p2PPbKGG.mjs → processes-Bn2WngOi.mjs} +1 -1
  43. package/dist/{processes-p2PPbKGG.mjs.map → processes-Bn2WngOi.mjs.map} +1 -1
  44. package/dist/{project-DXc7q4wN.mjs → project-yejJSfdJ.mjs} +57 -4
  45. package/dist/project-yejJSfdJ.mjs.map +1 -0
  46. package/dist/scaffold-manifest-CKNFLX9T.mjs +81 -0
  47. package/dist/scaffold-manifest-CKNFLX9T.mjs.map +1 -0
  48. package/dist/{sync-Bu_iLIbb.mjs → sync-Cx2_4rXW.mjs} +178 -52
  49. package/dist/sync-Cx2_4rXW.mjs.map +1 -0
  50. package/dist/user-schema-W1OsXdpc.mjs +91 -0
  51. package/dist/user-schema-W1OsXdpc.mjs.map +1 -0
  52. package/oclif.manifest.json +227 -5
  53. package/package.json +7 -5
  54. package/dist/docker-guidance-3UZESTfb.mjs +0 -21
  55. package/dist/docker-guidance-3UZESTfb.mjs.map +0 -1
  56. package/dist/oclif-SjHLB_XK.mjs.map +0 -1
  57. package/dist/orca-_TmrJQMw.mjs.map +0 -1
  58. package/dist/ports-XujDtA47.mjs.map +0 -1
  59. package/dist/project-DXc7q4wN.mjs.map +0 -1
  60. package/dist/sync-Bu_iLIbb.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,20 +1,51 @@
1
- import { D as toZitadelError, E as ZitadelError, T as stableStringify, b as publicCliCommand, n as DEFAULT_SERVER, t as BaseCommand } from "../oclif-SjHLB_XK.mjs";
2
- import { n as inspectScaffoldTarget, o as issuerFromPort, r as RENDERER_IDS, t as createOrca } from "../orca-_TmrJQMw.mjs";
3
- import { n as hasZitadelSecret, t as hasZitadelConfig } from "../project-DXc7q4wN.mjs";
4
- import { a as hashForState, c as updateState, s as writeBackResource, u as FLOWS_DIR } from "../sync-Bu_iLIbb.mjs";
5
- import { t as SCHEMAS_DIR } from "../user-schema-DDz5-lX5.mjs";
6
- import { n as verifyLoginAction, t as customizeAndPublishActions } from "../journey-guidance-E_3_UwGt.mjs";
7
- import { t as listListeningPorts } from "../ports-XujDtA47.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";
8
10
  import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
9
11
  import { basename, dirname, join } from "node:path";
10
12
  import { cancel, confirm, intro, isCancel, outro, select, spinner, text } from "@clack/prompts";
11
13
  import { Flags } from "@oclif/core";
12
14
  import { createZitadelClient } from "@zitadel/api/client";
13
- 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";
14
16
  import { consola as consola$1 } from "consola";
17
+ import { BRANDING_FILE_SCHEMA_REF } from "@zitadel/config/meta-schemas";
15
18
  import { spawn } from "node:child_process";
16
19
  import { normalizeFlowBody, normalizeSchemaBody } from "@zitadel/config/normalize";
17
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
18
49
  //#region src/lib/setup-resources.ts
19
50
  /**
20
51
  * Scaffolds the versioned local default resources for a new project, uploads
@@ -32,7 +63,11 @@ async function materializeSetupResources(opts) {
32
63
  await mkdir(join(opts.cwd, SCHEMAS_DIR), { recursive: true });
33
64
  const filesWritten = [];
34
65
  const preset = opts.preset ?? DEFAULT_SETUP_PRESET;
35
- 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
+ });
36
71
  if (await writeResourceFile(opts.cwd, DEFAULT_SCHEMA_CONFIG_PATH, schemaBody, opts.force)) filesWritten.push(join(opts.cwd, DEFAULT_SCHEMA_CONFIG_PATH));
37
72
  const schemaId = requiredString((await opts.client.createSchema(schemaBody, { project_id: opts.projectId })).id, "created schema id");
38
73
  let schemaHash = hashForState({ normalize: normalizeSchemaBody }, schemaBody);
@@ -48,7 +83,8 @@ async function materializeSetupResources(opts) {
48
83
  });
49
84
  const flowBody = getDefaultLoginFlow({
50
85
  userSchemaUrl: schemaId,
51
- preset
86
+ preset,
87
+ useCase
52
88
  });
53
89
  if (await writeResourceFile(opts.cwd, DEFAULT_FLOW_CONFIG_PATH, flowBody, opts.force)) filesWritten.push(join(opts.cwd, DEFAULT_FLOW_CONFIG_PATH));
54
90
  const flow = await opts.client.createFlowDefinition({
@@ -67,6 +103,24 @@ async function materializeSetupResources(opts) {
67
103
  name: flowBody.name,
68
104
  status: flowBody.status
69
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
+ }
70
124
  const schemasReadme = join(SCHEMAS_DIR, "README.md");
71
125
  const flowsReadme = join(FLOWS_DIR, "README.md");
72
126
  if (await writeReadmeFile(opts.cwd, schemasReadme, schemasReadmeContent())) filesWritten.push(join(opts.cwd, schemasReadme));
@@ -74,6 +128,22 @@ async function materializeSetupResources(opts) {
74
128
  return { filesWritten };
75
129
  }
76
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
+ /**
77
147
  * Write a README file, but never overwrite an existing one. A developer who
78
148
  * has edited the README should keep their edits when `setup --force` is
79
149
  * re-run.
@@ -295,6 +365,77 @@ function bail(value) {
295
365
  }
296
366
  }
297
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
298
439
  //#region src/commands/setup/prompts/dev-port.ts
299
440
  /**
300
441
  * "Dev server port" — defaults to the detected port. The validated answer
@@ -338,64 +479,27 @@ var FrameworkConfirmPrompt = class {
338
479
  }
339
480
  };
340
481
  //#endregion
341
- //#region src/lib/prober/http.ts
342
- /**
343
- * Fetch `url` with a per-call timeout and pass the resulting `Response` to
344
- * `predicate`. Returns the predicate's value, or `null` on any failure: a
345
- * network error, an `AbortError` from the timeout, a thrown predicate, or the
346
- * predicate returning `null`.
347
- *
348
- * Never throws. The predicate decides what counts as a match — it gets the
349
- * raw `Response` and may read `.json()` / `.headers` / `.status` as needed.
350
- *
351
- * `timeoutMs` defaults to 500ms. The implementation uses
352
- * `AbortSignal.timeout`, so the underlying fetch is cancelled when the
353
- * timeout fires (no orphaned sockets).
354
- */
355
- async function probeUrl(url, predicate, opts) {
356
- const timeoutMs = opts?.timeoutMs ?? 500;
357
- try {
358
- return await predicate(await fetch(url, { signal: AbortSignal.timeout(timeoutMs) }));
359
- } catch {
360
- return null;
361
- }
362
- }
363
- /**
364
- * Probe many URLs in parallel and return the non-null matches, preserving
365
- * the input order. Each URL is bounded by `timeoutMs` independently — one
366
- * slow target can't hold up the others.
367
- */
368
- async function probeUrls(urls, predicate, opts) {
369
- const list = [...urls];
370
- return (await Promise.all(list.map(async (url) => {
371
- const value = await probeUrl(url, predicate, opts);
372
- return value === null ? null : {
373
- url,
374
- value
375
- };
376
- }))).filter((match) => match !== null);
377
- }
378
- //#endregion
379
482
  //#region src/commands/setup/prompts/server.ts
380
483
  /** Sentinel returned by the choice select when the user picks "Custom URL". */
381
484
  const CUSTOM = "__custom__";
382
- /** Per-probe timeout for the localhost OIDC scan — generous enough for a TLS handshake on a busy laptop. */
383
- const OIDC_PROBE_TIMEOUT_MS = 300;
384
485
  /**
385
- * "Which server should `zitadel.json` point to?" — Zitadel Cloud, a localhost
386
- * OIDC server we discovered, or a custom URL.
486
+ * "Which server should `zitadel.json` point to?" — Zitadel Cloud, the managed
487
+ * local server we detected, or a custom URL.
387
488
  *
388
- * Before asking, it scans the loopback for listening ports (via the generic
389
- * `lib/prober`) and `GET`s `/.well-known/openid-configuration` on each. Every
390
- * 2xx response with a string `issuer` becomes an extra option in the choice
391
- * list. Picking a discovered URL writes it directly to `answers.server` and
392
- * skips the follow-up text prompt. Picking "Custom URL" still asks for a
489
+ * Before asking, it looks for a healthy local Zitadel server the same way the
490
+ * rest of the CLI does (`detectHealthyLocalServer`: the runtime metadata that
491
+ * `zitadel start` wrote to `.zitadel/local/runtime.json`, falling back to a
492
+ * `/healthz` probe on the default localhost port). A detected server becomes
493
+ * an extra option in the choice list *and* the preselected answer a user
494
+ * who just ran `zitadel start` almost certainly wants it, and `start`'s own
495
+ * next-step hint says `setup --server local`. Picking it writes the URL
496
+ * directly to `answers.server`; picking "Custom URL" still asks for a
393
497
  * validated URL exactly as before.
394
498
  */
395
499
  var ServerPrompt = class {
396
500
  async ask(answers, ctx) {
397
501
  if (ctx.serverFlag) return answers;
398
- const discovered = await discoverLocalOidc();
502
+ const detected = await detectLocalServer(ctx.cwd);
399
503
  const choice = await select({
400
504
  message: "Which server should zitadel.json point to?",
401
505
  options: [
@@ -404,17 +508,17 @@ var ServerPrompt = class {
404
508
  label: "Zitadel Cloud (api.zitadel.cloud)",
405
509
  hint: "recommended for real projects"
406
510
  },
407
- ...discovered.map((server) => ({
408
- value: server,
409
- label: server,
410
- hint: "detected — OIDC"
411
- })),
511
+ ...detected ? [{
512
+ value: detected,
513
+ label: `Local Zitadel server (${detected})`,
514
+ hint: "detected — started with zitadel start"
515
+ }] : [],
412
516
  {
413
517
  value: CUSTOM,
414
518
  label: "Custom URL (self-hosted)"
415
519
  }
416
520
  ],
417
- initialValue: answers.server ?? "https://api.zitadel.cloud"
521
+ initialValue: detected ?? answers.server ?? "https://api.zitadel.cloud"
418
522
  });
419
523
  bail(choice);
420
524
  if (choice !== CUSTOM) return {
@@ -441,37 +545,16 @@ var ServerPrompt = class {
441
545
  }
442
546
  };
443
547
  /**
444
- * Returns the loopback origins (`http://localhost:<port>`) whose
445
- * `/.well-known/openid-configuration` endpoint responds with a valid OIDC
446
- * discovery document. Sniffs purely via the generic prober — the only
447
- * Zitadel-shaped detail here is the OIDC predicate, which is the standard
448
- * "any OIDC server" contract (no `zitadel`-keyword check), so Keycloak/dex/
449
- * etc. also surface and the user picks the right one.
548
+ * Wraps {@link detectHealthyLocalServer} in a spinner: the unhealthy paths
549
+ * block on up to two 1.5s `/healthz` probes, long enough that the wizard
550
+ * should say what it is doing.
450
551
  */
451
- async function discoverLocalOidc() {
552
+ async function detectLocalServer(cwd) {
452
553
  const s = spinner();
453
- s.start("Scanning localhost for OIDC servers");
454
- try {
455
- const ports = await listListeningPorts();
456
- if (ports.length === 0) {
457
- s.stop("No local servers detected.");
458
- return [];
459
- }
460
- const origins = (await probeUrls(ports.map((port) => `http://localhost:${port}/.well-known/openid-configuration`), async (response) => {
461
- if (!response.ok) return null;
462
- try {
463
- const body = await response.json();
464
- return typeof body.issuer === "string" ? body.issuer : null;
465
- } catch {
466
- return null;
467
- }
468
- }, { timeoutMs: OIDC_PROBE_TIMEOUT_MS })).map((match) => new URL(match.url).origin);
469
- s.stop(origins.length === 0 ? "No local OIDC servers found." : `Found ${origins.length} local OIDC server${origins.length === 1 ? "" : "s"}.`);
470
- return origins;
471
- } catch (error) {
472
- s.stop("Discovery skipped.");
473
- throw error;
474
- }
554
+ s.start("Checking for a local Zitadel server");
555
+ const detected = await detectHealthyLocalServer(cwd);
556
+ s.stop(detected ? `Found local Zitadel server at ${detected}.` : "No local Zitadel server detected.");
557
+ return detected;
475
558
  }
476
559
  //#endregion
477
560
  //#region src/commands/setup/prompts/sign-in-preset.ts
@@ -504,6 +587,48 @@ var SignInPresetPrompt = class {
504
587
  }
505
588
  };
506
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
507
632
  //#region src/commands/setup/prompts/pick-framework.ts
508
633
  /**
509
634
  * "Choose a framework to scaffold" — the only prompt outside the main wizard.
@@ -543,7 +668,9 @@ const SETUP_PROMPTS = [
543
668
  new FrameworkConfirmPrompt(),
544
669
  new ServerPrompt(),
545
670
  new DevPortPrompt(),
546
- new SignInPresetPrompt()
671
+ new UseCasePrompt(),
672
+ new SignInPresetPrompt(),
673
+ new DesignPrompt()
547
674
  ];
548
675
  //#endregion
549
676
  //#region src/commands/setup/summary.ts
@@ -576,6 +703,8 @@ const path = (s) => pc.cyan(s);
576
703
  const url = (s) => pc.cyan(s);
577
704
  /** Yellow, for opaque ids the user shouldn't try to read. */
578
705
  const id = (s) => pc.yellow(s);
706
+ /** Dim, for separators and secondary detail. */
707
+ const dim = (s) => pc.dim(s);
579
708
  /**
580
709
  * Reads the project root to identify the framework version, TS presence,
581
710
  * and which package manager the user runs. Returns safe defaults (npm
@@ -645,6 +774,16 @@ function fileNameOf(p) {
645
774
  * load.
646
775
  */
647
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(", ")}.`;
648
787
  /** `zitadel setup` — create a project and scaffold local auth.
649
788
  *
650
789
  * Detects (or, for an empty directory, scaffolds then re-detects) the
@@ -666,14 +805,22 @@ var Setup = class Setup extends BaseCommand {
666
805
  options: FRAMEWORK_OPTIONS
667
806
  }),
668
807
  renderer: Flags.string({
669
- description: "Renderer (default: react).",
670
- options: [...RENDERER_IDS]
808
+ description: RENDERER_FLAG_DESCRIPTION,
809
+ options: [...AVAILABLE_RENDERER_IDS]
671
810
  }),
672
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." }),
673
812
  "skip-install": Flags.boolean({ description: "Do not install dependencies after setup updates package.json." }),
674
813
  preset: Flags.string({
675
814
  description: "Sign-in preset for the scaffolded schema and login flow (default: password-first).",
676
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]
677
824
  })
678
825
  };
679
826
  async run() {
@@ -713,6 +860,8 @@ var Setup = class Setup extends BaseCommand {
713
860
  skip_install: Boolean(flags["skip-install"]),
714
861
  dev_port_explicit: flags["dev-port"] !== void 0,
715
862
  preset: flags.preset ?? DEFAULT_SETUP_PRESET,
863
+ use_case: flags["use-case"] ?? DEFAULT_SETUP_USE_CASE,
864
+ design: flags.design ?? "built-in",
716
865
  step: "framework_resolved"
717
866
  });
718
867
  if (flags["dev-port"] !== void 0) {
@@ -727,20 +876,29 @@ var Setup = class Setup extends BaseCommand {
727
876
  let answers = {
728
877
  server: this.meta.source,
729
878
  devPort: framework.devPort,
730
- 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
731
882
  };
732
883
  if (!nonInteractive && !dryRun) {
733
884
  intro("Zitadel setup");
734
885
  const promptCtx = {
735
886
  framework,
887
+ cwd,
736
888
  serverFlag: this.meta.serverFlag,
737
889
  devPortFromFlag: flags["dev-port"] !== void 0,
738
- presetFromFlag: flags.preset !== void 0
890
+ presetFromFlag: flags.preset !== void 0,
891
+ useCaseFromFlag: flags["use-case"] !== void 0,
892
+ designFromFlag: flags.design !== void 0
739
893
  };
740
894
  for (const prompt of SETUP_PROMPTS) answers = await prompt.ask(answers, promptCtx);
741
895
  outro("Configuration captured");
742
896
  }
743
- this.recordTelemetry({ preset: answers.preset });
897
+ this.recordTelemetry({
898
+ preset: answers.preset,
899
+ use_case: answers.useCase,
900
+ design: answers.design ?? "built-in"
901
+ });
744
902
  const issuer = issuerFromPort(answers.devPort);
745
903
  framework = {
746
904
  ...framework,
@@ -754,10 +912,13 @@ var Setup = class Setup extends BaseCommand {
754
912
  ...retryOptionsFromFlags(flags),
755
913
  framework: framework.id,
756
914
  preset: answers.preset,
915
+ useCase: answers.useCase,
916
+ design: answers.design,
757
917
  devPort: answers.devPort
758
918
  });
759
919
  consola$1.success(`Created project ${project.id}`);
760
920
  this.recordTelemetry({ step: "project_created" });
921
+ const posture = derivePosture(framework.id, scaffoldedFramework);
761
922
  const ctx = {
762
923
  framework,
763
924
  rendererId: flags.renderer ?? "react",
@@ -766,7 +927,9 @@ var Setup = class Setup extends BaseCommand {
766
927
  server: answers.server,
767
928
  cliVersion: this.meta.cliVersion,
768
929
  scaffoldedFramework,
769
- preset: answers.preset
930
+ posture,
931
+ preset: answers.preset,
932
+ useCase: answers.useCase
770
933
  };
771
934
  consola$1.start(`Patching project files${dryRun ? " (dry run)" : ""}`);
772
935
  const result = await orca.patcherFor(framework.id).patch(ctx, {
@@ -785,11 +948,13 @@ var Setup = class Setup extends BaseCommand {
785
948
  cwd,
786
949
  client: createZitadelClient({
787
950
  baseUrl: answers.server,
788
- token: project.projectSecret
951
+ token: project.project_secret
789
952
  }),
790
953
  projectId: project.id,
791
954
  force,
792
- preset: answers.preset
955
+ preset: answers.preset,
956
+ useCase: answers.useCase,
957
+ design: answers.design
793
958
  });
794
959
  } catch (error) {
795
960
  await rm(join(cwd, "zitadel.json"), { force: true });
@@ -811,6 +976,21 @@ var Setup = class Setup extends BaseCommand {
811
976
  step: "files_patched",
812
977
  files_written_count: allFilesWritten.length
813
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
+ }
814
994
  const installOutcome = await installDependenciesForSetup({
815
995
  cliVersion: this.meta.cliVersion,
816
996
  cwd,
@@ -827,6 +1007,16 @@ var Setup = class Setup extends BaseCommand {
827
1007
  package_manager: installOutcome.install.package_manager
828
1008
  });
829
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
+ };
830
1020
  if (!this.jsonEnabled()) {
831
1021
  const sections = buildSummary({
832
1022
  projectFacts: await detectProjectFacts(cwd, framework.id),
@@ -834,14 +1024,15 @@ var Setup = class Setup extends BaseCommand {
834
1024
  project,
835
1025
  server: answers.server,
836
1026
  issuer,
837
- scaffoldedFramework
1027
+ scaffoldedFramework,
1028
+ design: answers.design
838
1029
  });
839
1030
  consola$1.box({
840
1031
  title: "Zitadel is ready",
841
1032
  message: [
842
1033
  renderSummary(sections),
843
1034
  "",
844
- installOutcome.boxActions.join("\n")
1035
+ [...installOutcome.boxActions, ...claimNudge.actions].join("\n")
845
1036
  ].join("\n"),
846
1037
  style: {
847
1038
  padding: 1,
@@ -862,10 +1053,20 @@ var Setup = class Setup extends BaseCommand {
862
1053
  framework: framework.id,
863
1054
  server: answers.server,
864
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
+ })),
865
1061
  files_skipped: result.filesSkipped.map((file) => relativeDisplay(cwd, file)),
866
1062
  install: installOutcome.install,
867
- next_actions: installOutcome.nextActions,
868
- 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]
869
1070
  }
870
1071
  });
871
1072
  }
@@ -894,10 +1095,10 @@ async function resolveScaffoldFramework(framework, nonInteractive, orca) {
894
1095
  function dryRunProject(issuer) {
895
1096
  return {
896
1097
  id: "dry-run-0000",
897
- projectSecret: "sk_proj_dry_run_full",
898
- previewSecret: "sk_proj_dry_run_preview",
899
- previewOrigins: [issuer],
900
- 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"
901
1102
  };
902
1103
  }
903
1104
  /**
@@ -909,6 +1110,8 @@ function setupRetryFlags(opts) {
909
1110
  const parts = [];
910
1111
  if (opts.framework) parts.push(`--framework ${opts.framework}`);
911
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}`);
912
1115
  if (opts.renderer && opts.renderer !== "react") parts.push(`--renderer ${opts.renderer}`);
913
1116
  if (opts.devPort !== void 0) parts.push(`--dev-port ${opts.devPort}`);
914
1117
  if (opts.nonInteractive) parts.push("--non-interactive");
@@ -924,6 +1127,8 @@ function retryOptionsFromFlags(flags) {
924
1127
  return {
925
1128
  framework: flags.framework,
926
1129
  preset: flags.preset,
1130
+ useCase: flags["use-case"],
1131
+ design: flags.design,
927
1132
  renderer: flags.renderer,
928
1133
  devPort: flags["dev-port"],
929
1134
  nonInteractive: Boolean(flags["non-interactive"])
@@ -933,8 +1138,8 @@ async function createProjectWithLocalHint(client, server, cliVersion, projectNam
933
1138
  try {
934
1139
  const payload = {
935
1140
  name: projectName,
936
- previewOrigins: [issuer],
937
- seedDefaults: false
1141
+ preview_origins: [issuer],
1142
+ seed_defaults: false
938
1143
  };
939
1144
  return await client.createProject(payload);
940
1145
  } catch (error) {
@@ -988,15 +1193,13 @@ function pickWrittenFile(written, suffix) {
988
1193
  }
989
1194
  /**
990
1195
  * Translates a patcher-written path into a single sentence the user can
991
- * read at narration speed. Returns `null` for directories and other
992
- * scaffolding artefacts that aren't worth narrating individually the
993
- * file count in the closing `success(...)` and the summary's INSTALLED
994
- * section already cover them. The verb tense flips for `--dry-run` so
995
- * the user sees a preview ("Would write ...") instead of a claim that
996
- * 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.
997
1201
  */
998
1202
  function describeWrittenFile(relPath, dryRun) {
999
- if (relPath === ".zitadel" || relPath === ".zitadel/flows" || relPath === ".zitadel/schemas" || relPath === ".zitadel/meta") return null;
1000
1203
  const verb = dryRun ? "Would write" : "Wrote";
1001
1204
  const sentence = SENTENCE_BY_PATH[relPath];
1002
1205
  if (sentence) return `${verb} ${sentence.subject} (${path(relPath)})`;
@@ -1035,7 +1238,7 @@ const SENTENCE_BY_PATH = {
1035
1238
  };
1036
1239
  /** Builds the section list driving {@link renderSummary} for the setup command. */
1037
1240
  function buildSummary(opts) {
1038
- const { projectFacts, writtenRel, project, server, issuer, scaffoldedFramework } = opts;
1241
+ const { projectFacts, writtenRel, project, server, issuer, scaffoldedFramework, design } = opts;
1039
1242
  const sdkPackage = "@zitadel/sdk-next";
1040
1243
  const packageJsonHit = pickWrittenFile(writtenRel, "package.json");
1041
1244
  const detected = [{
@@ -1079,6 +1282,14 @@ function buildSummary(opts) {
1079
1282
  {
1080
1283
  label: "App will run",
1081
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")
1082
1293
  }
1083
1294
  ];
1084
1295
  return [