@zitadel/cli 1.0.0-alpha.21 → 1.0.0-alpha.23

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 (64) hide show
  1. package/README.md +2248 -227
  2. package/SKILLS.md +246 -58
  3. package/dist/groups-CMB0fMzE.mjs +22 -0
  4. package/dist/groups-CMB0fMzE.mjs.map +1 -0
  5. package/dist/index.mjs +16027 -0
  6. package/dist/index.mjs.map +1 -0
  7. package/dist/lib/oclif/help.mjs +67 -0
  8. package/dist/lib/oclif/help.mjs.map +1 -0
  9. package/oclif.manifest.json +4577 -185
  10. package/package.json +42 -8
  11. package/dist/branding-cygSBPkV.mjs +0 -79
  12. package/dist/branding-cygSBPkV.mjs.map +0 -1
  13. package/dist/claim-state-DlfD6Res.mjs +0 -66
  14. package/dist/claim-state-DlfD6Res.mjs.map +0 -1
  15. package/dist/commands/apply.mjs +0 -81
  16. package/dist/commands/apply.mjs.map +0 -1
  17. package/dist/commands/branding/eject.mjs +0 -104
  18. package/dist/commands/branding/eject.mjs.map +0 -1
  19. package/dist/commands/claim.mjs +0 -415
  20. package/dist/commands/claim.mjs.map +0 -1
  21. package/dist/commands/doctor.mjs +0 -1122
  22. package/dist/commands/doctor.mjs.map +0 -1
  23. package/dist/commands/eject.mjs +0 -157
  24. package/dist/commands/eject.mjs.map +0 -1
  25. package/dist/commands/logs.mjs +0 -58
  26. package/dist/commands/logs.mjs.map +0 -1
  27. package/dist/commands/plan.mjs +0 -65
  28. package/dist/commands/plan.mjs.map +0 -1
  29. package/dist/commands/reset.mjs +0 -79
  30. package/dist/commands/reset.mjs.map +0 -1
  31. package/dist/commands/schemas/list.mjs +0 -146
  32. package/dist/commands/schemas/list.mjs.map +0 -1
  33. package/dist/commands/setup.mjs +0 -1271
  34. package/dist/commands/setup.mjs.map +0 -1
  35. package/dist/commands/start.mjs +0 -288
  36. package/dist/commands/start.mjs.map +0 -1
  37. package/dist/commands/status.mjs +0 -151
  38. package/dist/commands/status.mjs.map +0 -1
  39. package/dist/commands/stop.mjs +0 -105
  40. package/dist/commands/stop.mjs.map +0 -1
  41. package/dist/designs-Ckz18Dpo.mjs +0 -38
  42. package/dist/designs-Ckz18Dpo.mjs.map +0 -1
  43. package/dist/docker-Ev1Ggt9l.mjs +0 -436
  44. package/dist/docker-Ev1Ggt9l.mjs.map +0 -1
  45. package/dist/environment-rjRVkJjW.mjs +0 -17
  46. package/dist/environment-rjRVkJjW.mjs.map +0 -1
  47. package/dist/journey-guidance-BGrOX_gT.mjs +0 -40
  48. package/dist/journey-guidance-BGrOX_gT.mjs.map +0 -1
  49. package/dist/oclif-CanO3zdt.mjs +0 -1769
  50. package/dist/oclif-CanO3zdt.mjs.map +0 -1
  51. package/dist/orca-66vAxeIe.mjs +0 -3917
  52. package/dist/orca-66vAxeIe.mjs.map +0 -1
  53. package/dist/package-manager-DFdLcDx1.mjs +0 -194
  54. package/dist/package-manager-DFdLcDx1.mjs.map +0 -1
  55. package/dist/ports-BM20XIZb.mjs +0 -116
  56. package/dist/ports-BM20XIZb.mjs.map +0 -1
  57. package/dist/processes-DKHPkU8O.mjs +0 -120
  58. package/dist/processes-DKHPkU8O.mjs.map +0 -1
  59. package/dist/project-CHhot85s.mjs +0 -140
  60. package/dist/project-CHhot85s.mjs.map +0 -1
  61. package/dist/sync-BBlACKzT.mjs +0 -1633
  62. package/dist/sync-BBlACKzT.mjs.map +0 -1
  63. package/dist/user-schema-DTuOsdKE.mjs +0 -91
  64. package/dist/user-schema-DTuOsdKE.mjs.map +0 -1
@@ -1,1271 +0,0 @@
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-66vAxeIe.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-BBlACKzT.mjs";
11
- import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
12
- import { basename, dirname, join } from "node:path";
13
- import { cancel, confirm, intro, isCancel, outro, select, spinner, text } from "@clack/prompts";
14
- import { Flags } from "@oclif/core";
15
- import { createZitadelClient } from "@zitadel/api/client";
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";
17
- import { consola as consola$1 } from "consola";
18
- import { BRANDING_FILE_SCHEMA_REF } from "@zitadel/config/meta-schemas";
19
- import { normalizeFlowBody, normalizeSchemaBody } from "@zitadel/config/normalize";
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
49
- //#region src/lib/setup-resources.ts
50
- /**
51
- * Scaffolds the versioned local default resources for a new project, uploads
52
- * them through the schema/flow APIs, and seeds `.zitadel/state.json` with the
53
- * IDs, hashes, and flow metadata the sync engine expects. Setup calls this only
54
- * after the framework patcher has created `.zitadel/{flows,schemas}` and the
55
- * initial state file.
56
- *
57
- * The schema is uploaded without an `$id`: the server assigns an opaque id on
58
- * `POST /schemas`, and the flow file can only be rendered after that id comes
59
- * back because `flow_definition.user_schema` must reference it.
60
- */
61
- async function materializeSetupResources(opts) {
62
- await mkdir(join(opts.cwd, FLOWS_DIR), { recursive: true });
63
- await mkdir(join(opts.cwd, SCHEMAS_DIR), { recursive: true });
64
- const filesWritten = [];
65
- const preset = opts.preset ?? DEFAULT_SETUP_PRESET;
66
- const useCase = opts.useCase ?? DEFAULT_SETUP_USE_CASE;
67
- const { $id: _templateId, ...schemaBody } = getDefaultHumanUserSchema({
68
- preset,
69
- useCase
70
- });
71
- if (await writeResourceFile(opts.cwd, DEFAULT_SCHEMA_CONFIG_PATH, schemaBody, opts.force)) filesWritten.push(join(opts.cwd, DEFAULT_SCHEMA_CONFIG_PATH));
72
- const schemaId = requiredString((await opts.client.createSchema(schemaBody, { project_id: opts.projectId })).id, "created schema id");
73
- let schemaHash = hashForState({ normalize: normalizeSchemaBody }, schemaBody);
74
- try {
75
- const canonical = (await opts.client.getSchemaById(encodeURIComponent(schemaId))).schema;
76
- schemaHash = (await writeBackResource(opts.cwd, DEFAULT_SCHEMA_CONFIG_PATH, { normalize: normalizeSchemaBody }, canonical)).hash;
77
- } catch (err) {
78
- consola$1.debug(`fetch created schema ${schemaId} during setup failed:`, err);
79
- }
80
- await updateState(opts.cwd, DEFAULT_SCHEMA_CONFIG_PATH, {
81
- id: schemaId,
82
- hash: schemaHash
83
- });
84
- const flowBody = getDefaultLoginFlow({
85
- userSchemaUrl: schemaId,
86
- preset,
87
- useCase
88
- });
89
- if (await writeResourceFile(opts.cwd, DEFAULT_FLOW_CONFIG_PATH, flowBody, opts.force)) filesWritten.push(join(opts.cwd, DEFAULT_FLOW_CONFIG_PATH));
90
- const flow = await opts.client.createFlowDefinition({
91
- project_id: opts.projectId,
92
- schema_uri: DEFAULT_FLOW_SCHEMA_URI,
93
- flow_definition: flowBody
94
- });
95
- let flowHash = hashForState({ normalize: normalizeFlowBody }, flowBody);
96
- if (flow.flow_definition) flowHash = (await writeBackResource(opts.cwd, DEFAULT_FLOW_CONFIG_PATH, {
97
- normalize: normalizeFlowBody,
98
- normalizeWrite: normalizeFlowBody
99
- }, flow.flow_definition)).hash;
100
- await updateState(opts.cwd, DEFAULT_FLOW_CONFIG_PATH, {
101
- id: requiredString(flow.id, "created flow definition id"),
102
- hash: flowHash,
103
- name: flowBody.name,
104
- status: flowBody.status
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
- }
124
- const schemasReadme = join(SCHEMAS_DIR, "README.md");
125
- const flowsReadme = join(FLOWS_DIR, "README.md");
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));
128
- return { filesWritten };
129
- }
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
- /**
147
- * Write a README file, but never overwrite an existing one. A developer who
148
- * has edited the README should keep their edits when `setup --force` is
149
- * re-run.
150
- */
151
- async function writeReadmeFile(cwd, relPath, content) {
152
- const dest = join(cwd, relPath);
153
- await mkdir(dirname(dest), { recursive: true });
154
- try {
155
- await writeFile(dest, content, { flag: "wx" });
156
- return true;
157
- } catch (error) {
158
- if (isErrno(error, "EEXIST")) return false;
159
- throw error;
160
- }
161
- }
162
- async function writeResourceFile(cwd, relPath, body, force) {
163
- const contents = `${stableStringify(body)}\n`;
164
- try {
165
- await writeFile(join(cwd, relPath), contents, force ? void 0 : { flag: "wx" });
166
- return true;
167
- } catch (error) {
168
- 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." });
169
- throw error;
170
- }
171
- }
172
- function requiredString(value, label) {
173
- if (typeof value === "string" && value.length > 0) return value;
174
- throw new ZitadelError("E_VALIDATION", `Missing ${label} in server response.`);
175
- }
176
- function isErrno(error, code) {
177
- return typeof error === "object" && error !== null && "code" in error && error.code === code;
178
- }
179
- //#endregion
180
- //#region src/commands/setup/install.ts
181
- async function installDependenciesForSetup(input) {
182
- const packageManager = await detectPackageManager(input.cwd);
183
- const installCommand = installCommandFor(packageManager);
184
- const devCommand = devCommandFor(packageManager);
185
- if (!(input.scaffoldedFramework || input.depsAdded.length > 0)) return outcome({
186
- install: {
187
- status: "not-needed",
188
- package_manager: packageManager,
189
- command: installCommand.display,
190
- reason: "no-dependency-changes"
191
- },
192
- devCommand: devCommand.display,
193
- cliVersion: input.cliVersion,
194
- issuer: input.issuer,
195
- includeInstallCommand: false
196
- });
197
- if (input.dryRun || input.skipInstall) return outcome({
198
- install: {
199
- status: "skipped",
200
- package_manager: packageManager,
201
- command: installCommand.display,
202
- reason: input.dryRun ? "dry-run" : "skip-install"
203
- },
204
- devCommand: devCommand.display,
205
- cliVersion: input.cliVersion,
206
- issuer: input.issuer,
207
- includeInstallCommand: true
208
- });
209
- consola$1.start(`Installing dependencies with ${installCommand.display}`);
210
- try {
211
- await (input.run ?? runPackageCommand)(installCommand, {
212
- cwd: input.cwd,
213
- env: input.env,
214
- redirectStdoutToStderr: input.json
215
- });
216
- } catch (error) {
217
- throw installFailed(error, installCommand, devCommand, input.cwd);
218
- }
219
- consola$1.success("Installed dependencies");
220
- return outcome({
221
- install: {
222
- status: "completed",
223
- package_manager: packageManager,
224
- command: installCommand.display
225
- },
226
- devCommand: devCommand.display,
227
- cliVersion: input.cliVersion,
228
- issuer: input.issuer,
229
- includeInstallCommand: false
230
- });
231
- }
232
- function outcome(input) {
233
- const planCommand = publicCliCommand("plan", input.cliVersion);
234
- const statusCommand = publicCliCommand("status", input.cliVersion);
235
- const verifyActions = [
236
- ...input.includeInstallCommand ? [`Install dependencies: ${input.install.command}`] : [],
237
- `Start your project: ${input.devCommand} (then open ${input.issuer}/login)`,
238
- verifyLoginAction()
239
- ];
240
- const breadcrumb = `Once login works: ${statusCommand} shows your next steps; customizing is covered in your README's Zitadel section.`;
241
- return {
242
- install: input.install,
243
- devCommand: input.devCommand,
244
- boxActions: [...verifyActions, breadcrumb],
245
- nextActions: [...verifyActions, ...customizeAndPublishActions(input.cliVersion)],
246
- nextCommands: input.includeInstallCommand ? [
247
- input.install.command,
248
- input.devCommand,
249
- planCommand
250
- ] : [input.devCommand, planCommand]
251
- };
252
- }
253
- function installFailed(error, installCommand, devCommand, cwd) {
254
- return new ZitadelError("E_VALIDATION", `Dependency install failed: ${installCommand.display}`, {
255
- hint: `Run ${installCommand.display} in ${cwd}, then start the app with ${devCommand.display}.`,
256
- nextCommands: [installCommand.display],
257
- details: {
258
- command: installCommand.display,
259
- cwd,
260
- original: errorShape(error)
261
- }
262
- });
263
- }
264
- function errorShape(error) {
265
- if (error instanceof Error) return {
266
- name: error.name,
267
- message: error.message,
268
- code: error.code
269
- };
270
- return { message: String(error) };
271
- }
272
- //#endregion
273
- //#region src/commands/setup/prompts/cancel.ts
274
- /**
275
- * Converts a clack cancellation (Ctrl-C) into a thrown `E_VALIDATION` rather
276
- * than a partial answer. Every prompt funnels its clack return value through
277
- * this so the wizard never proceeds with a sentinel or missing value.
278
- */
279
- function bail(value) {
280
- if (isCancel(value)) {
281
- cancel("Setup cancelled.");
282
- throw new ZitadelError("E_VALIDATION", "Setup cancelled by user");
283
- }
284
- }
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
334
- //#region src/commands/setup/prompts/dev-port.ts
335
- /**
336
- * "Dev server port" — defaults to the detected port. The validated answer
337
- * becomes the issuer URL (`http://localhost:<port>`) via `issuerFromPort`.
338
- */
339
- var DevPortPrompt = class {
340
- async ask(answers, ctx) {
341
- if (ctx.devPortFromFlag) return answers;
342
- const value = await text({
343
- message: "Dev server port",
344
- placeholder: String(answers.devPort),
345
- initialValue: String(answers.devPort),
346
- validate: (input) => {
347
- const num = Number.parseInt(input ?? "", 10);
348
- return Number.isFinite(num) && num > 0 && num < 65536 ? void 0 : "Must be a port number";
349
- }
350
- });
351
- bail(value);
352
- return {
353
- ...answers,
354
- devPort: Number.parseInt(String(value), 10)
355
- };
356
- }
357
- };
358
- //#endregion
359
- //#region src/commands/setup/prompts/framework-confirm.ts
360
- /**
361
- * "Detected `<framework>`. Proceed?" — the wizard's first question. Accepting
362
- * leaves answers unchanged; declining throws `E_UNSUPPORTED_PROJECT_SHAPE`
363
- * (the user should re-run with an explicit `--framework`).
364
- */
365
- var FrameworkConfirmPrompt = class {
366
- async ask(answers, ctx) {
367
- const ack = await confirm({
368
- message: `Detected ${ctx.framework.id}. Proceed?`,
369
- initialValue: true
370
- });
371
- bail(ack);
372
- if (ack === false) throw new ZitadelError("E_UNSUPPORTED_PROJECT_SHAPE", "Setup cancelled — framework declined", { hint: `Re-run with --framework ${ctx.framework.id} when ready.` });
373
- return answers;
374
- }
375
- };
376
- //#endregion
377
- //#region src/commands/setup/prompts/server.ts
378
- /** Sentinel returned by the choice select when the user picks "Custom URL". */
379
- const CUSTOM = "__custom__";
380
- /**
381
- * "Which server should `zitadel.json` point to?" — Zitadel Cloud, the managed
382
- * local server we detected, or a custom URL.
383
- *
384
- * Before asking, it looks for a healthy local Zitadel server the same way the
385
- * rest of the CLI does (`detectHealthyLocalServer`: the runtime metadata that
386
- * `zitadel start` wrote to `.zitadel/local/runtime.json`, falling back to a
387
- * `/healthz` probe on the default localhost port). A detected server becomes
388
- * an extra option in the choice list *and* the preselected answer — a user
389
- * who just ran `zitadel start` almost certainly wants it, and `start`'s own
390
- * next-step hint says `setup --server local`. Picking it writes the URL
391
- * directly to `answers.server`; picking "Custom URL" still asks for a
392
- * validated URL exactly as before.
393
- */
394
- var ServerPrompt = class {
395
- async ask(answers, ctx) {
396
- if (ctx.serverFlag) return answers;
397
- const detected = await detectLocalServer(ctx.cwd);
398
- const choice = await select({
399
- message: "Which server should zitadel.json point to?",
400
- options: [
401
- {
402
- value: DEFAULT_SERVER,
403
- label: "Zitadel Cloud (api.zitadel.cloud)",
404
- hint: "recommended for real projects"
405
- },
406
- ...detected ? [{
407
- value: detected,
408
- label: `Local Zitadel server (${detected})`,
409
- hint: "detected — started with zitadel start"
410
- }] : [],
411
- {
412
- value: CUSTOM,
413
- label: "Custom URL (self-hosted)"
414
- }
415
- ],
416
- initialValue: detected ?? answers.server ?? "https://api.zitadel.cloud"
417
- });
418
- bail(choice);
419
- if (choice !== CUSTOM) return {
420
- ...answers,
421
- server: choice
422
- };
423
- const custom = await text({
424
- message: "Server URL",
425
- placeholder: "https://zitadel.internal",
426
- validate: (value) => {
427
- try {
428
- new URL(value ?? "");
429
- return;
430
- } catch {
431
- return "Must be a valid URL";
432
- }
433
- }
434
- });
435
- bail(custom);
436
- return {
437
- ...answers,
438
- server: custom
439
- };
440
- }
441
- };
442
- /**
443
- * Wraps {@link detectHealthyLocalServer} in a spinner: the unhealthy paths
444
- * block on up to two 1.5s `/healthz` probes, long enough that the wizard
445
- * should say what it is doing.
446
- */
447
- async function detectLocalServer(cwd) {
448
- const s = spinner();
449
- s.start("Checking for a local Zitadel server");
450
- const detected = await detectHealthyLocalServer(cwd);
451
- s.stop(detected ? `Found local Zitadel server at ${detected}.` : "No local Zitadel server detected.");
452
- return detected;
453
- }
454
- //#endregion
455
- //#region src/commands/setup/prompts/sign-in-preset.ts
456
- /**
457
- * "How should users sign in?" — picks the schema+flow preset the scaffold
458
- * starts from (#448: app-type/preset selection before any `.zitadel/` file
459
- * is written). `--preset` is authoritative; non-interactive runs keep the
460
- * password-first default seeded by the command.
461
- */
462
- var SignInPresetPrompt = class {
463
- async ask(answers, ctx) {
464
- if (ctx.presetFromFlag) return answers;
465
- const labels = {
466
- "password-first": "Password first — email + password, passkey optional during registration",
467
- "passkey-first": "Passkey first — one-tap passkey up front, email + password fallback"
468
- };
469
- const value = await select({
470
- message: "How should users sign in?",
471
- initialValue: answers.preset,
472
- options: SETUP_PRESETS.map((preset) => ({
473
- value: preset,
474
- label: labels[preset]
475
- }))
476
- });
477
- bail(value);
478
- return {
479
- ...answers,
480
- preset: value
481
- };
482
- }
483
- };
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
527
- //#region src/commands/setup/prompts/pick-framework.ts
528
- /**
529
- * "Choose a framework to scaffold" — the only prompt outside the main wizard.
530
- * Runs at the empty-directory branch (before any other detection), so it owns
531
- * its own `intro` heading. Returns the chosen framework id; choices come from
532
- * `Orca.availableFrameworks`.
533
- */
534
- var PickFrameworkPrompt = class {
535
- async ask(choices) {
536
- intro("Zitadel setup — new project");
537
- const picked = await select({
538
- message: "Choose a framework to scaffold",
539
- options: choices.map((choice) => ({
540
- value: choice.id,
541
- label: choice.displayName
542
- }))
543
- });
544
- bail(picked);
545
- return picked;
546
- }
547
- };
548
- //#endregion
549
- //#region src/commands/setup/prompts/index.ts
550
- /**
551
- * Public surface for the setup wizard prompts. The `setup` command imports
552
- * {@link SETUP_PROMPTS} and iterates every entry, threading the answers
553
- * through each prompt's `ask` so they decide whether to actually ask the
554
- * user. Add a new question by writing a class and appending an instance to
555
- * the registry below.
556
- *
557
- * {@link PickFrameworkPrompt} is intentionally **not** in {@link SETUP_PROMPTS}
558
- * — it runs at the empty-directory scaffold branch, before the main wizard
559
- * starts.
560
- */
561
- /** Every question the main setup wizard asks, in ask order. */
562
- const SETUP_PROMPTS = [
563
- new FrameworkConfirmPrompt(),
564
- new ServerPrompt(),
565
- new DevPortPrompt(),
566
- new UseCasePrompt(),
567
- new SignInPresetPrompt(),
568
- new DesignPrompt()
569
- ];
570
- //#endregion
571
- //#region src/commands/setup/summary.ts
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
- /**
583
- * Renders the section list as a single multi-line string. Labels are
584
- * padded to a common width per section so the `✓ label value` columns
585
- * line up, the title prints in dim gray, and `✓` is green. Values come
586
- * in pre-styled — use the helpers below ({@link path}, {@link url},
587
- * {@link id}, {@link dim}) to keep the colour palette consistent with
588
- * the mock.
589
- */
590
- function renderSummary(sections) {
591
- const lines = [];
592
- for (const section of sections) {
593
- if (section.rows.length === 0) continue;
594
- if (lines.length > 0) lines.push("");
595
- lines.push(pc.dim(section.title.toUpperCase()));
596
- const labelWidth = Math.max(...section.rows.map((r) => r.label.length)) + 3;
597
- for (const row of section.rows) {
598
- const labelCol = row.label.padEnd(labelWidth);
599
- const secondary = row.secondary ? ` ${pc.dim("→")} ${row.secondary}` : "";
600
- lines.push(`${pc.green("✓")} ${labelCol}${row.value}${secondary}`);
601
- }
602
- }
603
- return lines.join("\n");
604
- }
605
- /** Cyan, for filesystem paths the user can open. */
606
- const path = (s) => pc.cyan(s);
607
- /** Cyan, for URLs (browser-clickable in most terminals). */
608
- const url = (s) => pc.cyan(s);
609
- /** Yellow, for opaque ids the user shouldn't try to read. */
610
- const id = (s) => pc.yellow(s);
611
- /** Dim, for separators and secondary detail. */
612
- const dim = (s) => pc.dim(s);
613
- /**
614
- * Reads the project root to identify the framework version, TS presence,
615
- * and which package manager the user runs. Returns safe defaults (npm
616
- * fallback, no version) on any error so summary rendering can always proceed.
617
- */
618
- async function detectProjectFacts(cwd, frameworkId) {
619
- const facts = {
620
- framework: frameworkId,
621
- typescript: await fileExists(join(cwd, "tsconfig.json")),
622
- packageManager: await detectPackageManager(cwd)
623
- };
624
- try {
625
- const raw = await readFile(join(cwd, "package.json"), "utf8");
626
- const pj = JSON.parse(raw);
627
- const depPkg = depFromFramework(frameworkId);
628
- const range = pj.dependencies?.[depPkg] ?? pj.devDependencies?.[depPkg];
629
- if (range) facts.frameworkVersion = stripRange(range);
630
- } catch {}
631
- return facts;
632
- }
633
- /** Formats the project facts as the "Next.js 15 · TypeScript · npm" string the mock shows. */
634
- function formatFrameworkLine(facts) {
635
- const segments = [];
636
- const pretty = prettyFramework(facts.framework);
637
- segments.push(facts.frameworkVersion ? `${pretty} ${facts.frameworkVersion}` : pretty);
638
- if (facts.typescript) segments.push("TypeScript");
639
- segments.push(facts.packageManager);
640
- return segments.join(` ${pc.dim("·")} `);
641
- }
642
- async function fileExists(p) {
643
- try {
644
- await stat(p);
645
- return true;
646
- } catch {
647
- return false;
648
- }
649
- }
650
- function depFromFramework(framework) {
651
- switch (framework) {
652
- case "next": return "next";
653
- case "nuxt": return "nuxt";
654
- default: return framework;
655
- }
656
- }
657
- function prettyFramework(framework) {
658
- switch (framework) {
659
- case "next": return "Next.js";
660
- case "nuxt": return "Nuxt";
661
- default: return framework;
662
- }
663
- }
664
- /** Strips `^`/`~`/`>=`/`v` prefixes from an npm range so the version reads cleanly in the summary. */
665
- function stripRange(range) {
666
- const trimmed = range.replace(/^([\^~]|>=?|v)/, "").trim();
667
- return trimmed.split(/\s+/)[0] ?? trimmed;
668
- }
669
- /** Returns just the file name from a relative path, for the `→ filename` secondary detail in PACKAGE rows. */
670
- function fileNameOf(p) {
671
- return basename(p);
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
- }
696
- //#endregion
697
- //#region src/commands/setup/index.ts
698
- /**
699
- * The frameworks `--framework` accepts, derived from Orca's registry so the
700
- * flag can't drift from what the CLI can actually scaffold. `createOrca` is
701
- * pure (it only builds the in-memory registries), so this is safe at module
702
- * load.
703
- */
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(", ")}.`;
715
- /** `zitadel setup` — create a project and scaffold local auth.
716
- *
717
- * Detects (or, for an empty directory, scaffolds then re-detects) the
718
- * framework, runs the wizard prompts to fill in any answers not pre-supplied
719
- * by flags, creates the remote project without server fallback defaults,
720
- * patches the local files via `Orca`'s framework patcher, then scaffolds and
721
- * uploads editable schema/flow config from `.zitadel/**`.
722
- *
723
- * Every interactive question lives in {@link SETUP_PROMPTS} (the main wizard
724
- * — each entry is a small class) and {@link PickFrameworkPrompt} (the
725
- * empty-directory framework choice, before the main wizard).
726
- */
727
- var Setup = class Setup extends BaseCommand {
728
- static description = "Create a Zitadel project and scaffold local auth.";
729
- static examples = ["<%= config.bin %> setup --framework next", "<%= config.bin %> setup --framework react --dev-port 3000"];
730
- static flags = {
731
- framework: Flags.string({
732
- description: "Framework to target.",
733
- options: FRAMEWORK_OPTIONS
734
- }),
735
- renderer: Flags.string({
736
- description: RENDERER_FLAG_DESCRIPTION,
737
- options: [...AVAILABLE_RENDERER_IDS]
738
- }),
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." }),
740
- "skip-install": Flags.boolean({ description: "Do not install dependencies after setup updates package.json." }),
741
- preset: Flags.string({
742
- description: "Sign-in preset for the scaffolded schema and login flow (default: password-first).",
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]
752
- })
753
- };
754
- async run() {
755
- const { flags } = await this.parse(Setup);
756
- try {
757
- await this.toMeta(flags);
758
- } catch (error) {
759
- throw localSetupHint(error, retryOptionsFromFlags(flags), this.config.version);
760
- }
761
- const { cwd, nonInteractive, dryRun, force } = this.meta;
762
- if (await hasZitadelConfig(cwd)) return this.emit({
763
- status: "skipped",
764
- reason: "already-initialized"
765
- });
766
- if (await hasZitadelSecret(cwd)) throw new ZitadelError("E_CONFLICT", ".zitadel/secret exists without zitadel.json", { hint: "Move the secret aside or restore zitadel.json before running setup." });
767
- const orca = createOrca();
768
- consola$1.start(`Detecting framework in ${shortPath(cwd)}`);
769
- let framework;
770
- let scaffoldedFramework = false;
771
- try {
772
- framework = await orca.detect(cwd, flags.framework);
773
- consola$1.success(`Detected ${framework.id}${framework.devPort ? ` (dev port ${framework.devPort})` : ""}`);
774
- } catch (error) {
775
- if (error instanceof ZitadelError && error.code === "E_FRAMEWORK_NOT_DETECTED") {
776
- const target = await inspectScaffoldTarget(cwd);
777
- if (!target.scaffoldable) throw frameworkDetectionWithScaffoldTarget(error, cwd, target);
778
- consola$1.info("Fresh app directory — scaffolding a fresh project");
779
- framework = await orca.scaffold(cwd, await resolveScaffoldFramework(flags.framework, nonInteractive, orca));
780
- scaffoldedFramework = true;
781
- consola$1.success(`Scaffolded ${framework.id} skeleton`);
782
- } else throw error;
783
- }
784
- this.recordTelemetry({
785
- framework: framework.id,
786
- renderer: flags.renderer ?? "react",
787
- scaffolded_skeleton: scaffoldedFramework,
788
- skip_install: Boolean(flags["skip-install"]),
789
- dev_port_explicit: flags["dev-port"] !== void 0,
790
- preset: flags.preset ?? DEFAULT_SETUP_PRESET,
791
- use_case: flags["use-case"] ?? DEFAULT_SETUP_USE_CASE,
792
- design: flags.design ?? "built-in",
793
- step: "framework_resolved"
794
- });
795
- if (flags["dev-port"] !== void 0) {
796
- const devPort = flags["dev-port"];
797
- if (!Number.isInteger(devPort) || devPort < 1 || devPort > 65535) throw new ZitadelError("E_VALIDATION", `--dev-port must be an integer in 1..65535, got ${devPort}`);
798
- framework = {
799
- ...framework,
800
- devPort,
801
- url: issuerFromPort(devPort)
802
- };
803
- }
804
- let answers = {
805
- server: this.meta.source,
806
- devPort: framework.devPort,
807
- preset: flags.preset ?? DEFAULT_SETUP_PRESET,
808
- useCase: flags["use-case"] ?? DEFAULT_SETUP_USE_CASE,
809
- design: flags.design
810
- };
811
- if (!nonInteractive && !dryRun) {
812
- intro("Zitadel setup");
813
- const promptCtx = {
814
- framework,
815
- cwd,
816
- serverFlag: this.meta.serverFlag,
817
- devPortFromFlag: flags["dev-port"] !== void 0,
818
- presetFromFlag: flags.preset !== void 0,
819
- useCaseFromFlag: flags["use-case"] !== void 0,
820
- designFromFlag: flags.design !== void 0
821
- };
822
- for (const prompt of SETUP_PROMPTS) answers = await prompt.ask(answers, promptCtx);
823
- outro("Configuration captured");
824
- }
825
- this.recordTelemetry({
826
- preset: answers.preset,
827
- use_case: answers.useCase,
828
- design: answers.design ?? "built-in"
829
- });
830
- const issuer = issuerFromPort(answers.devPort);
831
- framework = {
832
- ...framework,
833
- devPort: answers.devPort,
834
- url: issuer
835
- };
836
- consola$1.start(`Creating project on ${answers.server}${dryRun ? " (dry run)" : ""}`);
837
- const unauthClient = createZitadelClient({ baseUrl: answers.server });
838
- const projectName = defaultProjectName(cwd, framework.id);
839
- const project = dryRun ? dryRunProject(issuer) : await createProjectWithLocalHint(unauthClient, answers.server, this.meta.cliVersion, projectName, issuer, {
840
- ...retryOptionsFromFlags(flags),
841
- framework: framework.id,
842
- preset: answers.preset,
843
- useCase: answers.useCase,
844
- design: answers.design,
845
- devPort: answers.devPort
846
- });
847
- consola$1.success(`Created project ${project.id}`);
848
- this.recordTelemetry({ step: "project_created" });
849
- const posture = derivePosture(framework.id, scaffoldedFramework);
850
- const ctx = {
851
- framework,
852
- rendererId: flags.renderer ?? "react",
853
- project,
854
- issuer,
855
- server: answers.server,
856
- cliVersion: this.meta.cliVersion,
857
- scaffoldedFramework,
858
- posture,
859
- preset: answers.preset,
860
- useCase: answers.useCase
861
- };
862
- consola$1.start(`Patching project files${dryRun ? " (dry run)" : ""}`);
863
- const result = await orca.patcherFor(framework.id).patch(ctx, {
864
- cwd,
865
- dryRun,
866
- force
867
- });
868
- for (const file of result.filesWritten) {
869
- const sentence = describeWrittenFile(relativeDisplayPath(cwd, file), dryRun);
870
- if (sentence) consola$1.info(sentence);
871
- }
872
- for (const file of result.filesSkipped) consola$1.info(`Left ${relativeDisplayPath(cwd, file)} unchanged (already matches target)`);
873
- let resourceResult;
874
- try {
875
- resourceResult = dryRun ? { filesWritten: [] } : await materializeSetupResources({
876
- cwd,
877
- client: createZitadelClient({
878
- baseUrl: answers.server,
879
- token: project.project_secret
880
- }),
881
- projectId: project.id,
882
- force,
883
- preset: answers.preset,
884
- useCase: answers.useCase,
885
- design: answers.design,
886
- cliVersion: this.meta.cliVersion
887
- });
888
- } catch (error) {
889
- await rm(join(cwd, "zitadel.json"), { force: true });
890
- await rm(join(cwd, ".zitadel/secret"), { force: true });
891
- const cause = toZitadelError(error);
892
- throw new ZitadelError(cause.code, `Default resource setup failed: ${cause.message}`, {
893
- hint: "The project was created but its default schema/flow upload did not finish. Re-run `zitadel setup` to start over (add --force to overwrite partially written .zitadel files).",
894
- nextCommands: ["zitadel setup --force"],
895
- details: cause.details
896
- });
897
- }
898
- for (const file of resourceResult.filesWritten) {
899
- const sentence = describeWrittenFile(relativeDisplayPath(cwd, file), dryRun);
900
- if (sentence) consola$1.info(sentence);
901
- }
902
- const allFilesWritten = [...result.filesWritten, ...resourceResult.filesWritten];
903
- consola$1.success(`Patched ${allFilesWritten.length} file${allFilesWritten.length === 1 ? "" : "s"}` + (result.filesSkipped.length > 0 ? ` (${result.filesSkipped.length} unchanged)` : ""));
904
- this.recordTelemetry({
905
- step: "files_patched",
906
- files_written_count: allFilesWritten.length
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
- }
923
- const installOutcome = await installDependenciesForSetup({
924
- cliVersion: this.meta.cliVersion,
925
- cwd,
926
- depsAdded: result.depsAdded,
927
- dryRun,
928
- env: this.meta.env,
929
- issuer,
930
- json: this.jsonEnabled(),
931
- scaffoldedFramework,
932
- skipInstall: Boolean(flags["skip-install"])
933
- });
934
- this.recordTelemetry({
935
- step: "dependencies_installed",
936
- package_manager: installOutcome.install.package_manager
937
- });
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);
950
- if (!this.jsonEnabled()) {
951
- const sections = buildSummary({
952
- projectFacts: await detectProjectFacts(cwd, framework.id),
953
- writtenRel,
954
- project,
955
- server: answers.server,
956
- issuer,
957
- scaffoldedFramework,
958
- design: answers.design
959
- });
960
- consola$1.box({
961
- title: "Zitadel is ready",
962
- message: [
963
- renderSummary(sections),
964
- "",
965
- [...installOutcome.boxActions, ...claimNudge.actions].join("\n")
966
- ].join("\n"),
967
- style: {
968
- padding: 1,
969
- borderStyle: "rounded",
970
- borderColor: "green"
971
- }
972
- });
973
- for (const warning of warnings) consola$1.warn(warning);
974
- }
975
- return this.emit({
976
- status: "ok",
977
- warnings,
978
- pretty: "",
979
- data: {
980
- title: "Zitadel is ready.",
981
- project: {
982
- project_id: project.id,
983
- issuer
984
- },
985
- framework: framework.id,
986
- server: answers.server,
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)),
994
- install: installOutcome.install,
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]
1002
- }
1003
- });
1004
- }
1005
- };
1006
- function frameworkDetectionWithScaffoldTarget(error, cwd, target) {
1007
- return new ZitadelError(error.code, "Could not detect a supported app framework, and this directory is not a fresh scaffold target", {
1008
- hint: `${target.reason ?? "Directory is not empty."} Run setup from an empty directory to scaffold a new app, or run setup from an existing supported app project.`,
1009
- details: {
1010
- cwd,
1011
- entries: target.entries,
1012
- reason: target.reason
1013
- }
1014
- });
1015
- }
1016
- /**
1017
- * Resolves which framework to scaffold into an empty directory: the explicit
1018
- * `--framework`, else PickFrameworkPrompt, else a hard error in non-interactive
1019
- * mode (an agent must pass `--framework`).
1020
- */
1021
- async function resolveScaffoldFramework(framework, nonInteractive, orca) {
1022
- if (framework) return framework;
1023
- if (nonInteractive) throw new ZitadelError("E_FRAMEWORK_NOT_DETECTED", "Empty directory — pass --framework", { hint: "Run without --json/--non-interactive to choose from a prompt, or pass --framework for scripted setup." });
1024
- return new PickFrameworkPrompt().ask(orca.availableFrameworks());
1025
- }
1026
- /** A deterministic stand-in project for `--dry-run`, so no remote call is made. */
1027
- function dryRunProject(issuer) {
1028
- return {
1029
- id: "dry-run-0000",
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"
1034
- };
1035
- }
1036
- /**
1037
- * Reconstructs the flag list of the current invocation for retry guidance,
1038
- * ending in `--server local`. Flags whose resolved value equals the default
1039
- * are omitted — the retry reproduces the same outcome without them.
1040
- */
1041
- function setupRetryFlags(opts) {
1042
- const parts = [];
1043
- if (opts.framework) parts.push(`--framework ${opts.framework}`);
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}`);
1047
- if (opts.renderer && opts.renderer !== "react") parts.push(`--renderer ${opts.renderer}`);
1048
- if (opts.devPort !== void 0) parts.push(`--dev-port ${opts.devPort}`);
1049
- if (opts.nonInteractive) parts.push("--non-interactive");
1050
- parts.push("--server local");
1051
- return parts.join(" ");
1052
- }
1053
- /**
1054
- * Retry options straight from parsed flags, for failures before the wizard
1055
- * resolves answers. `--non-interactive` is echoed only when explicitly passed
1056
- * — TTY/JSON-inferred non-interactivity re-infers itself on the retry.
1057
- */
1058
- function retryOptionsFromFlags(flags) {
1059
- return {
1060
- framework: flags.framework,
1061
- preset: flags.preset,
1062
- useCase: flags["use-case"],
1063
- design: flags.design,
1064
- renderer: flags.renderer,
1065
- devPort: flags["dev-port"],
1066
- nonInteractive: Boolean(flags["non-interactive"])
1067
- };
1068
- }
1069
- async function createProjectWithLocalHint(client, server, cliVersion, projectName, issuer, retry) {
1070
- try {
1071
- const payload = {
1072
- name: projectName,
1073
- preview_origins: [issuer],
1074
- seed_defaults: false
1075
- };
1076
- return await client.createProject(payload);
1077
- } catch (error) {
1078
- const normalized = toZitadelError(error);
1079
- const retryFlags = setupRetryFlags(retry);
1080
- throw new ZitadelError(normalized.code, normalized.message, {
1081
- hint: `${normalized.hint ? `${normalized.hint} ` : ""}If you meant to use a local Zitadel server, start it first and retry setup with ${retryFlags}.`,
1082
- nextCommands: [publicCliCommand("start", cliVersion), publicCliCommand(`setup ${retryFlags}`, cliVersion)],
1083
- details: {
1084
- server,
1085
- original: normalized.details
1086
- }
1087
- });
1088
- }
1089
- }
1090
- function defaultProjectName(cwd, framework) {
1091
- const fromDirectory = basename(cwd).trim();
1092
- return fromDirectory.length > 0 ? fromDirectory : `zitadel-${framework}-app`;
1093
- }
1094
- function localSetupHint(error, retry, cliVersion) {
1095
- const normalized = toZitadelError(error);
1096
- if (normalized.code !== "E_LOCAL_SERVER_NOT_RUNNING") return error;
1097
- const setupCommand = `setup ${setupRetryFlags(retry)}`;
1098
- return new ZitadelError(normalized.code, normalized.message, {
1099
- hint: `${normalized.hint ? `${normalized.hint} ` : ""}Start local Zitadel first, then rerun setup. After setup succeeds, follow its next_commands to start the app and verify registration, logout, and login in the browser.`,
1100
- nextCommands: [publicCliCommand("start", cliVersion), publicCliCommand(setupCommand, cliVersion)],
1101
- details: normalized.details
1102
- });
1103
- }
1104
- /**
1105
- * Replaces the user's `$HOME` with `~` in a path for compact terminal output.
1106
- * Falls back to the raw path when `HOME` isn't set or doesn't match.
1107
- */
1108
- function shortPath(absolute) {
1109
- const home = process.env.HOME;
1110
- if (home && absolute.startsWith(home)) return `~${absolute.slice(home.length)}`;
1111
- return absolute;
1112
- }
1113
- /**
1114
- * Picks one written file by suffix so the corresponding INSTALLED row can
1115
- * reference it without hard-coding the path the patcher chose. Returns
1116
- * the first match; falls back to `undefined` when the patcher didn't
1117
- * write that artifact (e.g. a renderer without a register page).
1118
- */
1119
- function pickWrittenFile(written, suffix) {
1120
- return written.find((file) => file.endsWith(suffix));
1121
- }
1122
- /**
1123
- * Translates a patcher-written path into a single sentence the user can
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.
1129
- */
1130
- function describeWrittenFile(relPath, dryRun) {
1131
- const verb = dryRun ? "Would write" : "Wrote";
1132
- const sentence = SENTENCE_BY_PATH[relPath];
1133
- if (sentence) return `${verb} ${sentence.subject} (${path(relPath)})`;
1134
- return `${verb} ${path(relPath)}`;
1135
- }
1136
- /**
1137
- * Map from the patcher's deterministic output paths to a short noun
1138
- * phrase describing what the file is for. Anything not in the map falls
1139
- * back to the bare path in the narration; add an entry here when a new
1140
- * scaffolded file deserves a clearer label.
1141
- */
1142
- const SENTENCE_BY_PATH = {
1143
- ".gitignore": { subject: "the project's .gitignore additions" },
1144
- ".zitadel/secret": { subject: "the local project secret" },
1145
- "zitadel.json": { subject: "the Zitadel project configuration" },
1146
- ".env.example": { subject: "the .env example template" },
1147
- ".env.local": { subject: "the local development environment variables" },
1148
- ".zitadel/state.json": { subject: "the sync state file" },
1149
- ".zitadel/flows/default-login.json": { subject: "the editable default login flow" },
1150
- ".zitadel/flows/README.md": { subject: "the flows folder README" },
1151
- ".zitadel/schemas/default-human-user.json": { subject: "the editable default human user schema" },
1152
- ".zitadel/schemas/README.md": { subject: "the schemas folder README" },
1153
- ".zitadel/meta/flow-definition.json": { subject: "the flow dialect spec (editor $schema)" },
1154
- ".zitadel/meta/user-schema.json": { subject: "the user-schema dialect spec" },
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" },
1160
- "AGENTS.md": { subject: "the agent guidance (golden journey + config dialect)" },
1161
- "README.md": { subject: "the README's Zitadel section" },
1162
- "app/page.tsx": { subject: "the home page redirect" },
1163
- "app/login/page.tsx": { subject: "the login page" },
1164
- "app/register/page.tsx": { subject: "the registration page" },
1165
- "app/profile/page.tsx": { subject: "the profile page" },
1166
- "middleware.ts": { subject: "the Next.js middleware" },
1167
- "proxy.ts": { subject: "the Next.js proxy" },
1168
- "custom-elements.d.ts": { subject: "the web-component type declarations" },
1169
- "package.json": { subject: "package.json with the SDK dependency" }
1170
- };
1171
- /** Builds the section list driving {@link renderSummary} for the setup command. */
1172
- function buildSummary(opts) {
1173
- const { projectFacts, writtenRel, project, server, issuer, scaffoldedFramework, design } = opts;
1174
- const sdkPackage = "@zitadel/sdk-next";
1175
- const packageJsonHit = pickWrittenFile(writtenRel, "package.json");
1176
- const detected = [{
1177
- label: "Framework",
1178
- value: formatFrameworkLine(projectFacts)
1179
- }];
1180
- if (scaffoldedFramework) detected.push({
1181
- label: "Scaffold",
1182
- value: "fresh project (no existing files)"
1183
- });
1184
- const installedRows = [];
1185
- if (packageJsonHit) installedRows.push({
1186
- label: "Package",
1187
- value: sdkPackage,
1188
- secondary: path(fileNameOf(packageJsonHit))
1189
- });
1190
- for (const [label, suffix] of [
1191
- ["Home redirect", "app/page.tsx"],
1192
- ["Login page", "app/login/page.tsx"],
1193
- ["Register page", "app/register/page.tsx"],
1194
- ["Profile page", "app/profile/page.tsx"],
1195
- ["Request proxy", "proxy.ts"],
1196
- ["Middleware", "middleware.ts"],
1197
- ["Env vars", ".env.local"]
1198
- ]) {
1199
- const hit = pickWrittenFile(writtenRel, suffix);
1200
- if (hit) installedRows.push({
1201
- label,
1202
- value: path(hit)
1203
- });
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
- });
1227
- const projectRows = [
1228
- {
1229
- label: "Project id",
1230
- value: id(project.id)
1231
- },
1232
- {
1233
- label: "Server",
1234
- value: url(server)
1235
- },
1236
- {
1237
- label: "App will run",
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")
1247
- }
1248
- ];
1249
- return [
1250
- {
1251
- title: "Detected",
1252
- rows: detected
1253
- },
1254
- {
1255
- title: "Installed",
1256
- rows: installedRows
1257
- },
1258
- {
1259
- title: "Customize",
1260
- rows: customizeRows
1261
- },
1262
- {
1263
- title: "Project",
1264
- rows: projectRows
1265
- }
1266
- ];
1267
- }
1268
- //#endregion
1269
- export { Setup as default };
1270
-
1271
- //# sourceMappingURL=setup.mjs.map