@zitadel/cli 1.0.0-alpha.22 → 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 (66) hide show
  1. package/README.md +2248 -227
  2. package/SKILLS.md +237 -65
  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 +40 -8
  11. package/dist/box-B_mwKole.mjs +0 -35
  12. package/dist/box-B_mwKole.mjs.map +0 -1
  13. package/dist/branding-DvoH8e0D.mjs +0 -79
  14. package/dist/branding-DvoH8e0D.mjs.map +0 -1
  15. package/dist/claim-state-B6ZSKmhW.mjs +0 -125
  16. package/dist/claim-state-B6ZSKmhW.mjs.map +0 -1
  17. package/dist/commands/apply.mjs +0 -81
  18. package/dist/commands/apply.mjs.map +0 -1
  19. package/dist/commands/branding/eject.mjs +0 -104
  20. package/dist/commands/branding/eject.mjs.map +0 -1
  21. package/dist/commands/claim.mjs +0 -445
  22. package/dist/commands/claim.mjs.map +0 -1
  23. package/dist/commands/doctor.mjs +0 -1138
  24. package/dist/commands/doctor.mjs.map +0 -1
  25. package/dist/commands/eject.mjs +0 -157
  26. package/dist/commands/eject.mjs.map +0 -1
  27. package/dist/commands/logs.mjs +0 -58
  28. package/dist/commands/logs.mjs.map +0 -1
  29. package/dist/commands/plan.mjs +0 -65
  30. package/dist/commands/plan.mjs.map +0 -1
  31. package/dist/commands/reset.mjs +0 -79
  32. package/dist/commands/reset.mjs.map +0 -1
  33. package/dist/commands/schemas/list.mjs +0 -146
  34. package/dist/commands/schemas/list.mjs.map +0 -1
  35. package/dist/commands/setup.mjs +0 -1309
  36. package/dist/commands/setup.mjs.map +0 -1
  37. package/dist/commands/start.mjs +0 -288
  38. package/dist/commands/start.mjs.map +0 -1
  39. package/dist/commands/status.mjs +0 -151
  40. package/dist/commands/status.mjs.map +0 -1
  41. package/dist/commands/stop.mjs +0 -105
  42. package/dist/commands/stop.mjs.map +0 -1
  43. package/dist/designs-Ckz18Dpo.mjs +0 -38
  44. package/dist/designs-Ckz18Dpo.mjs.map +0 -1
  45. package/dist/docker-B1MjOMFo.mjs +0 -436
  46. package/dist/docker-B1MjOMFo.mjs.map +0 -1
  47. package/dist/environment-fibXmzM8.mjs +0 -17
  48. package/dist/environment-fibXmzM8.mjs.map +0 -1
  49. package/dist/journey-guidance-C9MxJNTg.mjs +0 -40
  50. package/dist/journey-guidance-C9MxJNTg.mjs.map +0 -1
  51. package/dist/oclif-JlsN1lfO.mjs +0 -1769
  52. package/dist/oclif-JlsN1lfO.mjs.map +0 -1
  53. package/dist/orca-CPMHzmg3.mjs +0 -3917
  54. package/dist/orca-CPMHzmg3.mjs.map +0 -1
  55. package/dist/package-manager-CDUsoGBy.mjs +0 -194
  56. package/dist/package-manager-CDUsoGBy.mjs.map +0 -1
  57. package/dist/ports-hidBnSW-.mjs +0 -116
  58. package/dist/ports-hidBnSW-.mjs.map +0 -1
  59. package/dist/processes-Cv19mJ2I.mjs +0 -120
  60. package/dist/processes-Cv19mJ2I.mjs.map +0 -1
  61. package/dist/project-Ba0nriNm.mjs +0 -140
  62. package/dist/project-Ba0nriNm.mjs.map +0 -1
  63. package/dist/sync-ChlLQ4vZ.mjs +0 -1633
  64. package/dist/sync-ChlLQ4vZ.mjs.map +0 -1
  65. package/dist/user-schema-C2mC-JTN.mjs +0 -91
  66. package/dist/user-schema-C2mC-JTN.mjs.map +0 -1
@@ -1,1309 +0,0 @@
1
- import { n as brandingDesignLabel, t as BRANDING_DESIGN_INFO } from "../designs-Ckz18Dpo.mjs";
2
- import { n as wrapForBox, t as renderBoxActions } from "../box-B_mwKole.mjs";
3
- import { D as toZitadelError, E as ZitadelError, M as serverKind, T as stableStringify, j as publicCliCommand, k as normalizePublicCliProse, n as DEFAULT_SERVER, t as BaseCommand, u as detectHealthyLocalServer } from "../oclif-JlsN1lfO.mjs";
4
- import { i as claimState, n as claimBoxAction, o as claimWindowDeadline, r as claimCommand, t as claimAction } from "../claim-state-B6ZSKmhW.mjs";
5
- import { n as customizeAndPublishActions, r as verifyLoginAction, t as brandingGuidanceAction } from "../journey-guidance-C9MxJNTg.mjs";
6
- import { i as RENDERER_IDS, n as inspectScaffoldTarget, r as AVAILABLE_RENDERER_IDS, s as issuerFromPort, t as createOrca } from "../orca-CPMHzmg3.mjs";
7
- import { n as hasZitadelSecret, t as hasZitadelConfig } from "../project-Ba0nriNm.mjs";
8
- import { a as updateState, t as SCHEMAS_DIR } from "../user-schema-C2mC-JTN.mjs";
9
- import { a as runPackageCommand, c as writeScaffoldManifest, i as installCommandFor, n as detectPackageManager, r as devCommandFor } from "../package-manager-CDUsoGBy.mjs";
10
- import { i as toBrandingWireBody, t as BRANDING_DIR } from "../branding-DvoH8e0D.mjs";
11
- import { c as writeBackResource, o as hashForState, u as FLOWS_DIR } from "../sync-ChlLQ4vZ.mjs";
12
- import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
13
- import { basename, dirname, join } from "node:path";
14
- import { cancel, confirm, intro, isCancel, outro, select, spinner, text } from "@clack/prompts";
15
- import { Flags } from "@oclif/core";
16
- import { createZitadelClient } from "@zitadel/api/client";
17
- 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";
18
- import { consola as consola$1 } from "consola";
19
- import pc from "picocolors";
20
- import { BRANDING_FILE_SCHEMA_REF } from "@zitadel/config/meta-schemas";
21
- import { normalizeFlowBody, normalizeSchemaBody } from "@zitadel/config/normalize";
22
- //#region src/lib/orca/patchers/posture.ts
23
- /**
24
- * Frameworks whose patchers add route files without owning the app shell —
25
- * the only ones where a pre-existing app keeps a layout for the widget
26
- * posture to inherit (ADR 044). The SPA families write the app's root
27
- * component, so nothing survives for a widget to embed into; they keep the
28
- * page posture until a non-destructive route/layout insertion contract
29
- * exists for their routers.
30
- */
31
- const ROUTE_BASED_FRAMEWORKS = new Set(["next", "nuxt"]);
32
- /**
33
- * The default embedding posture of the scaffolded auth/profile pages
34
- * (ADR 044), derived from the same hinge as the framework homepage: whether
35
- * setup created the app skeleton itself. A fresh scaffold has no design to
36
- * respect — full-page chrome is the strongest start. A pre-existing
37
- * route-based app has its own shell and theme, so the pages embed
38
- * `variant="widget"` cards in a layout-neutral wrapper instead of painting
39
- * token-colored chrome underneath the host layout.
40
- *
41
- * Derived once at setup time and recorded in the scaffold manifest;
42
- * `doctor --fix` restores from the record rather than re-deriving (a
43
- * manifest-less legacy scaffold could not answer the hinge).
44
- */
45
- function derivePosture(frameworkId, scaffoldedFramework) {
46
- if (!ROUTE_BASED_FRAMEWORKS.has(frameworkId)) return "page";
47
- return scaffoldedFramework ? "page" : "widget";
48
- }
49
- //#endregion
50
- //#region src/lib/setup-resources.ts
51
- /**
52
- * Scaffolds the versioned local default resources for a new project, uploads
53
- * them through the schema/flow APIs, and seeds `.zitadel/state.json` with the
54
- * IDs, hashes, and flow metadata the sync engine expects. Setup calls this only
55
- * after the framework patcher has created `.zitadel/{flows,schemas}` and the
56
- * initial state file.
57
- *
58
- * The schema is uploaded without an `$id`: the server assigns an opaque id on
59
- * `POST /schemas`, and the flow file can only be rendered after that id comes
60
- * back because `flow_definition.user_schema` must reference it.
61
- */
62
- async function materializeSetupResources(opts) {
63
- await mkdir(join(opts.cwd, FLOWS_DIR), { recursive: true });
64
- await mkdir(join(opts.cwd, SCHEMAS_DIR), { recursive: true });
65
- const filesWritten = [];
66
- const preset = opts.preset ?? DEFAULT_SETUP_PRESET;
67
- const useCase = opts.useCase ?? DEFAULT_SETUP_USE_CASE;
68
- const { $id: _templateId, ...schemaBody } = getDefaultHumanUserSchema({
69
- preset,
70
- useCase
71
- });
72
- if (await writeResourceFile(opts.cwd, DEFAULT_SCHEMA_CONFIG_PATH, schemaBody, opts.force)) filesWritten.push(join(opts.cwd, DEFAULT_SCHEMA_CONFIG_PATH));
73
- const schemaId = requiredString((await opts.client.createSchema(schemaBody, { project_id: opts.projectId })).id, "created schema id");
74
- let schemaHash = hashForState({ normalize: normalizeSchemaBody }, schemaBody);
75
- try {
76
- const canonical = (await opts.client.getSchemaById(encodeURIComponent(schemaId))).schema;
77
- schemaHash = (await writeBackResource(opts.cwd, DEFAULT_SCHEMA_CONFIG_PATH, { normalize: normalizeSchemaBody }, canonical)).hash;
78
- } catch (err) {
79
- consola$1.debug(`fetch created schema ${schemaId} during setup failed:`, err);
80
- }
81
- await updateState(opts.cwd, DEFAULT_SCHEMA_CONFIG_PATH, {
82
- id: schemaId,
83
- hash: schemaHash
84
- });
85
- const flowBody = getDefaultLoginFlow({
86
- userSchemaUrl: schemaId,
87
- preset,
88
- useCase
89
- });
90
- if (await writeResourceFile(opts.cwd, DEFAULT_FLOW_CONFIG_PATH, flowBody, opts.force)) filesWritten.push(join(opts.cwd, DEFAULT_FLOW_CONFIG_PATH));
91
- const flow = await opts.client.createFlowDefinition({
92
- project_id: opts.projectId,
93
- schema_uri: DEFAULT_FLOW_SCHEMA_URI,
94
- flow_definition: flowBody
95
- });
96
- let flowHash = hashForState({ normalize: normalizeFlowBody }, flowBody);
97
- if (flow.flow_definition) flowHash = (await writeBackResource(opts.cwd, DEFAULT_FLOW_CONFIG_PATH, {
98
- normalize: normalizeFlowBody,
99
- normalizeWrite: normalizeFlowBody
100
- }, flow.flow_definition)).hash;
101
- await updateState(opts.cwd, DEFAULT_FLOW_CONFIG_PATH, {
102
- id: requiredString(flow.id, "created flow definition id"),
103
- hash: flowHash,
104
- name: flowBody.name,
105
- status: flowBody.status
106
- });
107
- if (opts.design) {
108
- await mkdir(join(opts.cwd, BRANDING_DIR), { recursive: true });
109
- const { branding, template } = getDefaultBrandingConfig(opts.design);
110
- const descriptor = {
111
- $schema: BRANDING_FILE_SCHEMA_REF,
112
- ...branding
113
- };
114
- if (await writeResourceFile(opts.cwd, DEFAULT_BRANDING_CONFIG_PATH, descriptor, opts.force)) filesWritten.push(join(opts.cwd, DEFAULT_BRANDING_CONFIG_PATH));
115
- if (await writeRawFile(opts.cwd, DEFAULT_BRANDING_TEMPLATE_PATH, template, opts.force)) filesWritten.push(join(opts.cwd, DEFAULT_BRANDING_TEMPLATE_PATH));
116
- const brandingNormalize = (data) => toBrandingWireBody(opts.cwd, data);
117
- const created = await opts.client.createBranding(brandingNormalize(descriptor), { project_id: opts.projectId });
118
- await updateState(opts.cwd, DEFAULT_BRANDING_CONFIG_PATH, {
119
- id: requiredString(created.id, "created branding revision id"),
120
- hash: hashForState({ normalize: brandingNormalize }, descriptor)
121
- });
122
- const brandingReadme = join(BRANDING_DIR, "README.md");
123
- if (await writeReadmeFile(opts.cwd, brandingReadme, normalizePublicCliProse(brandingReadmeContent(), opts.cliVersion))) filesWritten.push(join(opts.cwd, brandingReadme));
124
- }
125
- const schemasReadme = join(SCHEMAS_DIR, "README.md");
126
- const flowsReadme = join(FLOWS_DIR, "README.md");
127
- if (await writeReadmeFile(opts.cwd, schemasReadme, normalizePublicCliProse(schemasReadmeContent(), opts.cliVersion))) filesWritten.push(join(opts.cwd, schemasReadme));
128
- if (await writeReadmeFile(opts.cwd, flowsReadme, normalizePublicCliProse(flowsReadmeContent(), opts.cliVersion))) filesWritten.push(join(opts.cwd, flowsReadme));
129
- return { filesWritten };
130
- }
131
- /**
132
- * Write a non-JSON scaffold file (the `.liquid` template) with the same
133
- * conflict semantics as {@link writeResourceFile}: `--force` overwrites,
134
- * otherwise an existing file is an `E_CONFLICT`.
135
- */
136
- async function writeRawFile(cwd, relPath, content, force) {
137
- const dest = join(cwd, relPath);
138
- await mkdir(dirname(dest), { recursive: true });
139
- try {
140
- await writeFile(dest, content, force ? void 0 : { flag: "wx" });
141
- return true;
142
- } catch (error) {
143
- 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." });
144
- throw error;
145
- }
146
- }
147
- /**
148
- * Write a README file, but never overwrite an existing one. A developer who
149
- * has edited the README should keep their edits when `setup --force` is
150
- * re-run.
151
- */
152
- async function writeReadmeFile(cwd, relPath, content) {
153
- const dest = join(cwd, relPath);
154
- await mkdir(dirname(dest), { recursive: true });
155
- try {
156
- await writeFile(dest, content, { flag: "wx" });
157
- return true;
158
- } catch (error) {
159
- if (isErrno(error, "EEXIST")) return false;
160
- throw error;
161
- }
162
- }
163
- async function writeResourceFile(cwd, relPath, body, force) {
164
- const contents = `${stableStringify(body)}\n`;
165
- try {
166
- await writeFile(join(cwd, relPath), contents, force ? void 0 : { flag: "wx" });
167
- return true;
168
- } catch (error) {
169
- 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." });
170
- throw error;
171
- }
172
- }
173
- function requiredString(value, label) {
174
- if (typeof value === "string" && value.length > 0) return value;
175
- throw new ZitadelError("E_VALIDATION", `Missing ${label} in server response.`);
176
- }
177
- function isErrno(error, code) {
178
- return typeof error === "object" && error !== null && "code" in error && error.code === code;
179
- }
180
- //#endregion
181
- //#region src/commands/setup/install.ts
182
- async function installDependenciesForSetup(input) {
183
- const packageManager = await detectPackageManager(input.cwd);
184
- const installCommand = installCommandFor(packageManager);
185
- const devCommand = devCommandFor(packageManager);
186
- if (!(input.scaffoldedFramework || input.depsAdded.length > 0)) return outcome({
187
- install: {
188
- status: "not-needed",
189
- package_manager: packageManager,
190
- command: installCommand.display,
191
- reason: "no-dependency-changes"
192
- },
193
- devCommand: devCommand.display,
194
- cliVersion: input.cliVersion,
195
- issuer: input.issuer,
196
- includeInstallCommand: false
197
- });
198
- if (input.dryRun || input.skipInstall) return outcome({
199
- install: {
200
- status: "skipped",
201
- package_manager: packageManager,
202
- command: installCommand.display,
203
- reason: input.dryRun ? "dry-run" : "skip-install"
204
- },
205
- devCommand: devCommand.display,
206
- cliVersion: input.cliVersion,
207
- issuer: input.issuer,
208
- includeInstallCommand: true
209
- });
210
- consola$1.start(`Installing dependencies with ${installCommand.display}`);
211
- try {
212
- await (input.run ?? runPackageCommand)(installCommand, {
213
- cwd: input.cwd,
214
- env: input.env,
215
- redirectStdoutToStderr: input.json
216
- });
217
- } catch (error) {
218
- throw installFailed(error, installCommand, devCommand, input.cwd);
219
- }
220
- consola$1.success("Installed dependencies");
221
- return outcome({
222
- install: {
223
- status: "completed",
224
- package_manager: packageManager,
225
- command: installCommand.display
226
- },
227
- devCommand: devCommand.display,
228
- cliVersion: input.cliVersion,
229
- issuer: input.issuer,
230
- includeInstallCommand: false
231
- });
232
- }
233
- function outcome(input) {
234
- const planCommand = publicCliCommand("plan", input.cliVersion);
235
- const statusCommand = publicCliCommand("status", input.cliVersion);
236
- const verifyActions = [
237
- ...input.includeInstallCommand ? [`Install dependencies: ${input.install.command}`] : [],
238
- `Start your project: ${input.devCommand} (then open ${input.issuer}/login)`,
239
- verifyLoginAction()
240
- ];
241
- const boxActions = [
242
- ...input.includeInstallCommand ? [{
243
- text: "Install dependencies:",
244
- command: input.install.command
245
- }] : [],
246
- {
247
- text: `Start your project (then open ${input.issuer}/login):`,
248
- command: input.devCommand
249
- },
250
- { text: verifyLoginAction() },
251
- {
252
- text: "Once login works, this shows your next steps (customizing is covered in your README's Zitadel section):",
253
- command: statusCommand
254
- }
255
- ];
256
- return {
257
- install: input.install,
258
- devCommand: input.devCommand,
259
- boxActions,
260
- nextActions: [...verifyActions, ...customizeAndPublishActions(input.cliVersion)],
261
- nextCommands: input.includeInstallCommand ? [
262
- input.install.command,
263
- input.devCommand,
264
- planCommand
265
- ] : [input.devCommand, planCommand]
266
- };
267
- }
268
- function installFailed(error, installCommand, devCommand, cwd) {
269
- return new ZitadelError("E_VALIDATION", `Dependency install failed: ${installCommand.display}`, {
270
- hint: `Run ${installCommand.display} in ${cwd}, then start the app with ${devCommand.display}.`,
271
- nextCommands: [installCommand.display],
272
- details: {
273
- command: installCommand.display,
274
- cwd,
275
- original: errorShape(error)
276
- }
277
- });
278
- }
279
- function errorShape(error) {
280
- if (error instanceof Error) return {
281
- name: error.name,
282
- message: error.message,
283
- code: error.code
284
- };
285
- return { message: String(error) };
286
- }
287
- //#endregion
288
- //#region src/commands/setup/prompts/cancel.ts
289
- /**
290
- * Converts a clack cancellation (Ctrl-C) into a thrown `E_VALIDATION` rather
291
- * than a partial answer. Every prompt funnels its clack return value through
292
- * this so the wizard never proceeds with a sentinel or missing value.
293
- */
294
- function bail(value) {
295
- if (isCancel(value)) {
296
- cancel("Setup cancelled.");
297
- throw new ZitadelError("E_VALIDATION", "Setup cancelled by user");
298
- }
299
- }
300
- //#endregion
301
- //#region src/commands/setup/prompts/design.ts
302
- /**
303
- * Sentinel select value for "keep the built-in login" — clack option values
304
- * must be strings, and `undefined` in {@link SetupAnswers.design} is the
305
- * real representation of that choice.
306
- */
307
- const BUILT_IN = "built-in";
308
- /**
309
- * "How should the login look?" — the last wizard question (#676). Asks after
310
- * the sign-in preset so the order tells a story: the use case owns what is
311
- * collected, the preset owns how users authenticate, the design owns how
312
- * that experience looks.
313
- *
314
- * The built-in default is preselected and writes nothing. Picking a starter
315
- * design forks its Liquid template into `.zitadel/branding/` and publishes
316
- * it as branding revision 1 — from then on the template is repo-owned and
317
- * stops tracking built-in improvements, so the option labels make the
318
- * file-write explicit and keep ownership an opt-in. `--design` is
319
- * authoritative; non-interactive runs keep the built-in template.
320
- */
321
- var DesignPrompt = class {
322
- async ask(answers, ctx) {
323
- if (ctx.designFromFlag) return answers;
324
- const value = await select({
325
- message: "How should the login look? A starter design adds its editable template to .zitadel/branding/.",
326
- initialValue: answers.design ?? BUILT_IN,
327
- options: [{
328
- value: BUILT_IN,
329
- label: "Built-in",
330
- hint: "no files added; pick a design anytime later with `branding eject`"
331
- }, ...BRANDING_DESIGNS.map((design) => ({
332
- value: design,
333
- label: BRANDING_DESIGN_INFO[design].label,
334
- hint: BRANDING_DESIGN_INFO[design].hint
335
- }))]
336
- });
337
- bail(value);
338
- if (value === BUILT_IN) return {
339
- ...answers,
340
- design: void 0
341
- };
342
- return {
343
- ...answers,
344
- design: value
345
- };
346
- }
347
- };
348
- //#endregion
349
- //#region src/commands/setup/prompts/dev-port.ts
350
- /**
351
- * "Dev server port" — defaults to the detected port. The validated answer
352
- * becomes the issuer URL (`http://localhost:<port>`) via `issuerFromPort`.
353
- */
354
- var DevPortPrompt = class {
355
- async ask(answers, ctx) {
356
- if (ctx.devPortFromFlag) return answers;
357
- const value = await text({
358
- message: "Dev server port",
359
- placeholder: String(answers.devPort),
360
- initialValue: String(answers.devPort),
361
- validate: (input) => {
362
- const num = Number.parseInt(input ?? "", 10);
363
- return Number.isFinite(num) && num > 0 && num < 65536 ? void 0 : "Must be a port number";
364
- }
365
- });
366
- bail(value);
367
- return {
368
- ...answers,
369
- devPort: Number.parseInt(String(value), 10)
370
- };
371
- }
372
- };
373
- //#endregion
374
- //#region src/commands/setup/prompts/framework-confirm.ts
375
- /**
376
- * "Detected `<framework>`. Proceed?" — the wizard's first question. Accepting
377
- * leaves answers unchanged; declining throws `E_UNSUPPORTED_PROJECT_SHAPE`
378
- * (the user should re-run with an explicit `--framework`).
379
- */
380
- var FrameworkConfirmPrompt = class {
381
- async ask(answers, ctx) {
382
- const ack = await confirm({
383
- message: `Detected ${ctx.framework.id}. Proceed?`,
384
- initialValue: true
385
- });
386
- bail(ack);
387
- if (ack === false) throw new ZitadelError("E_UNSUPPORTED_PROJECT_SHAPE", "Setup cancelled — framework declined", { hint: `Re-run with --framework ${ctx.framework.id} when ready.` });
388
- return answers;
389
- }
390
- };
391
- //#endregion
392
- //#region src/commands/setup/prompts/server.ts
393
- /** Sentinel returned by the choice select when the user picks "Custom URL". */
394
- const CUSTOM = "__custom__";
395
- /**
396
- * "Which server should `zitadel.json` point to?" — Zitadel Cloud, the managed
397
- * local server we detected, or a custom URL.
398
- *
399
- * Before asking, it looks for a healthy local Zitadel server the same way the
400
- * rest of the CLI does (`detectHealthyLocalServer`: the runtime metadata that
401
- * `zitadel start` wrote to `.zitadel/local/runtime.json`, falling back to a
402
- * `/healthz` probe on the default localhost port). A detected server becomes
403
- * an extra option in the choice list *and* the preselected answer — a user
404
- * who just ran `zitadel start` almost certainly wants it, and `start`'s own
405
- * next-step hint says `setup --server local`. Picking it writes the URL
406
- * directly to `answers.server`; picking "Custom URL" still asks for a
407
- * validated URL exactly as before.
408
- */
409
- var ServerPrompt = class {
410
- async ask(answers, ctx) {
411
- if (ctx.serverFlag) return answers;
412
- const detected = await detectLocalServer(ctx.cwd);
413
- const choice = await select({
414
- message: "Which server should zitadel.json point to?",
415
- options: [
416
- {
417
- value: DEFAULT_SERVER,
418
- label: "Zitadel Cloud (api.zitadel.cloud)",
419
- hint: "recommended for real projects"
420
- },
421
- ...detected ? [{
422
- value: detected,
423
- label: `Local Zitadel server (${detected})`,
424
- hint: "detected — started with zitadel start"
425
- }] : [],
426
- {
427
- value: CUSTOM,
428
- label: "Custom URL (self-hosted)"
429
- }
430
- ],
431
- initialValue: detected ?? answers.server ?? "https://api.zitadel.cloud"
432
- });
433
- bail(choice);
434
- if (choice !== CUSTOM) return {
435
- ...answers,
436
- server: choice
437
- };
438
- const custom = await text({
439
- message: "Server URL",
440
- placeholder: "https://zitadel.internal",
441
- validate: (value) => {
442
- try {
443
- new URL(value ?? "");
444
- return;
445
- } catch {
446
- return "Must be a valid URL";
447
- }
448
- }
449
- });
450
- bail(custom);
451
- return {
452
- ...answers,
453
- server: custom
454
- };
455
- }
456
- };
457
- /**
458
- * Wraps {@link detectHealthyLocalServer} in a spinner: the unhealthy paths
459
- * block on up to two 1.5s `/healthz` probes, long enough that the wizard
460
- * should say what it is doing.
461
- */
462
- async function detectLocalServer(cwd) {
463
- const s = spinner();
464
- s.start("Checking for a local Zitadel server");
465
- const detected = await detectHealthyLocalServer(cwd);
466
- s.stop(detected ? `Found local Zitadel server at ${detected}.` : "No local Zitadel server detected.");
467
- return detected;
468
- }
469
- //#endregion
470
- //#region src/commands/setup/prompts/sign-in-preset.ts
471
- /**
472
- * "How should users sign in?" — picks the schema+flow preset the scaffold
473
- * starts from (#448: app-type/preset selection before any `.zitadel/` file
474
- * is written). `--preset` is authoritative; non-interactive runs keep the
475
- * password-first default seeded by the command.
476
- */
477
- var SignInPresetPrompt = class {
478
- async ask(answers, ctx) {
479
- if (ctx.presetFromFlag) return answers;
480
- const labels = {
481
- "password-first": "Password first — email + password, passkey optional during registration",
482
- "passkey-first": "Passkey first — one-tap passkey up front, email + password fallback"
483
- };
484
- const value = await select({
485
- message: "How should users sign in?",
486
- initialValue: answers.preset,
487
- options: SETUP_PRESETS.map((preset) => ({
488
- value: preset,
489
- label: labels[preset]
490
- }))
491
- });
492
- bail(value);
493
- return {
494
- ...answers,
495
- preset: value
496
- };
497
- }
498
- };
499
- //#endregion
500
- //#region src/commands/setup/prompts/use-case.ts
501
- /**
502
- * "Who will sign in to your app?" — picks the use case, which owns the
503
- * schema field set the scaffold collects (#448). Orthogonal to the sign-in
504
- * preset, so this asks first: what a user *is* before *how* they sign in.
505
- * `--use-case` is authoritative; non-interactive runs keep the minimal
506
- * default seeded by the command.
507
- */
508
- var UseCasePrompt = class {
509
- async ask(answers, ctx) {
510
- if (ctx.useCaseFromFlag) return answers;
511
- const options = {
512
- minimal: {
513
- label: "Just me or a small group",
514
- hint: "collects email"
515
- },
516
- consumer: {
517
- label: "Consumers",
518
- hint: "collects email, given & family name"
519
- },
520
- business: {
521
- label: "Business",
522
- hint: "collects email, given & family name, company"
523
- }
524
- };
525
- const value = await select({
526
- message: "Who will sign in to your app? Collected fields stay editable after setup.",
527
- initialValue: answers.useCase,
528
- options: SETUP_USE_CASES.map((useCase) => ({
529
- value: useCase,
530
- label: options[useCase].label,
531
- hint: options[useCase].hint
532
- }))
533
- });
534
- bail(value);
535
- return {
536
- ...answers,
537
- useCase: value
538
- };
539
- }
540
- };
541
- //#endregion
542
- //#region src/commands/setup/prompts/pick-framework.ts
543
- /**
544
- * "Choose a framework to scaffold" — the only prompt outside the main wizard.
545
- * Runs at the empty-directory branch (before any other detection), so it owns
546
- * its own `intro` heading. Returns the chosen framework id; choices come from
547
- * `Orca.availableFrameworks`.
548
- */
549
- var PickFrameworkPrompt = class {
550
- async ask(choices) {
551
- intro("Zitadel setup — new project");
552
- const picked = await select({
553
- message: "Choose a framework to scaffold",
554
- options: choices.map((choice) => ({
555
- value: choice.id,
556
- label: choice.displayName
557
- }))
558
- });
559
- bail(picked);
560
- return picked;
561
- }
562
- };
563
- //#endregion
564
- //#region src/commands/setup/prompts/index.ts
565
- /**
566
- * Public surface for the setup wizard prompts. The `setup` command imports
567
- * {@link SETUP_PROMPTS} and iterates every entry, threading the answers
568
- * through each prompt's `ask` so they decide whether to actually ask the
569
- * user. Add a new question by writing a class and appending an instance to
570
- * the registry below.
571
- *
572
- * {@link PickFrameworkPrompt} is intentionally **not** in {@link SETUP_PROMPTS}
573
- * — it runs at the empty-directory scaffold branch, before the main wizard
574
- * starts.
575
- */
576
- /** Every question the main setup wizard asks, in ask order. */
577
- const SETUP_PROMPTS = [
578
- new FrameworkConfirmPrompt(),
579
- new ServerPrompt(),
580
- new DevPortPrompt(),
581
- new UseCasePrompt(),
582
- new SignInPresetPrompt(),
583
- new DesignPrompt()
584
- ];
585
- //#endregion
586
- //#region src/commands/setup/summary.ts
587
- /**
588
- * Renders a path under `cwd` with stable POSIX separators. Setup's summary
589
- * matches generated artifacts against repository-style suffixes such as
590
- * `.zitadel/schemas/default-human-user.json`; normalizing here keeps those
591
- * matches working when Node returns Windows paths with backslashes.
592
- */
593
- function relativeDisplayPath(cwd, absolute) {
594
- const boundary = absolute[cwd.length];
595
- return (absolute.startsWith(cwd) && (boundary === "/" || boundary === "\\") ? absolute.slice(cwd.length + 1) : absolute).replaceAll("\\", "/");
596
- }
597
- /**
598
- * Renders the section list as a single multi-line string. Labels are
599
- * padded to a common width per section so the `✓ label value` columns
600
- * line up, the title prints in dim gray, and `✓` is green. Values come
601
- * in pre-styled — use the helpers below ({@link path}, {@link url},
602
- * {@link id}, {@link dim}) to keep the colour palette consistent with
603
- * the mock.
604
- */
605
- function renderSummary(sections) {
606
- const lines = [];
607
- for (const section of sections) {
608
- if (section.rows.length === 0) continue;
609
- if (lines.length > 0) lines.push("");
610
- lines.push(pc.dim(section.title.toUpperCase()));
611
- const labelWidth = Math.max(...section.rows.map((r) => r.label.length)) + 3;
612
- for (const row of section.rows) {
613
- const labelCol = row.label.padEnd(labelWidth);
614
- const secondary = row.secondary ? ` ${pc.dim("→")} ${row.secondary}` : "";
615
- lines.push(`${pc.green("✓")} ${labelCol}${row.value}${secondary}`);
616
- }
617
- }
618
- return lines.join("\n");
619
- }
620
- /** Cyan, for filesystem paths the user can open. */
621
- const path = (s) => pc.cyan(s);
622
- /** Cyan, for URLs (browser-clickable in most terminals). */
623
- const url = (s) => pc.cyan(s);
624
- /** Yellow, for opaque ids the user shouldn't try to read. */
625
- const id = (s) => pc.yellow(s);
626
- /** Dim, for separators and secondary detail. */
627
- const dim = (s) => pc.dim(s);
628
- /**
629
- * Reads the project root to identify the framework version, TS presence,
630
- * and which package manager the user runs. Returns safe defaults (npm
631
- * fallback, no version) on any error so summary rendering can always proceed.
632
- */
633
- async function detectProjectFacts(cwd, frameworkId) {
634
- const facts = {
635
- framework: frameworkId,
636
- typescript: await fileExists(join(cwd, "tsconfig.json")),
637
- packageManager: await detectPackageManager(cwd)
638
- };
639
- try {
640
- const raw = await readFile(join(cwd, "package.json"), "utf8");
641
- const pj = JSON.parse(raw);
642
- const depPkg = depFromFramework(frameworkId);
643
- const range = pj.dependencies?.[depPkg] ?? pj.devDependencies?.[depPkg];
644
- if (range) facts.frameworkVersion = stripRange(range);
645
- } catch {}
646
- return facts;
647
- }
648
- /** Formats the project facts as the "Next.js 15 · TypeScript · npm" string the mock shows. */
649
- function formatFrameworkLine(facts) {
650
- const segments = [];
651
- const pretty = prettyFramework(facts.framework);
652
- segments.push(facts.frameworkVersion ? `${pretty} ${facts.frameworkVersion}` : pretty);
653
- if (facts.typescript) segments.push("TypeScript");
654
- segments.push(facts.packageManager);
655
- return segments.join(` ${pc.dim("·")} `);
656
- }
657
- async function fileExists(p) {
658
- try {
659
- await stat(p);
660
- return true;
661
- } catch {
662
- return false;
663
- }
664
- }
665
- function depFromFramework(framework) {
666
- switch (framework) {
667
- case "next": return "next";
668
- case "nuxt": return "nuxt";
669
- default: return framework;
670
- }
671
- }
672
- function prettyFramework(framework) {
673
- switch (framework) {
674
- case "next": return "Next.js";
675
- case "nuxt": return "Nuxt";
676
- default: return framework;
677
- }
678
- }
679
- /** Strips `^`/`~`/`>=`/`v` prefixes from an npm range so the version reads cleanly in the summary. */
680
- function stripRange(range) {
681
- const trimmed = range.replace(/^([\^~]|>=?|v)/, "").trim();
682
- return trimmed.split(/\s+/)[0] ?? trimmed;
683
- }
684
- /** Returns just the file name from a relative path, for the `→ filename` secondary detail in PACKAGE rows. */
685
- function fileNameOf(p) {
686
- return basename(p);
687
- }
688
- /**
689
- * Layout caveats for the chosen login design, shown to humans after the
690
- * summary box and carried in the JSON envelope's `warnings`.
691
- *
692
- * The split family's brand pane is keyed to the **login's own container**
693
- * width — a `@container (max-width: 48rem)` query on the widget's mount, not
694
- * a viewport media query. Above that the pane renders; below it the pane is
695
- * `display: none` and the compact brand mark takes its place — and the split
696
- * template only emits that mark when `branding.json` names `logo_url` or
697
- * `hero_url`, so without one the narrow layout loses the branding entirely.
698
- *
699
- * Independent of posture, because the container query is: a full-page login
700
- * hits the same collapse on a phone that an embedded card hits in a sidebar.
701
- * Gating on `widget` would have been a guess about the container, and a wrong
702
- * one in both directions — the wrapper setup scaffolds for a widget is
703
- * full-width (the pane renders there), and a page-posture login on a phone is
704
- * exactly the narrow case the advice is for. `hero` stays quiet: its compact
705
- * fallback is editable text, so a narrow container never leaves it blank.
706
- */
707
- function designWarnings(design) {
708
- if (design !== "split" && design !== "split-right") return [];
709
- 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.`];
710
- }
711
- //#endregion
712
- //#region src/commands/setup/index.ts
713
- /**
714
- * The frameworks `--framework` accepts, derived from Orca's registry so the
715
- * flag can't drift from what the CLI can actually scaffold. `createOrca` is
716
- * pure (it only builds the in-memory registries), so this is safe at module
717
- * load.
718
- */
719
- const FRAMEWORK_OPTIONS = createOrca().availableFrameworks().map((framework) => framework.id);
720
- /**
721
- * `--renderer` offers only ids `getRenderer` will resolve: a
722
- * declared-but-unpublished renderer (ADR 006) keeps its registry entry to
723
- * reserve the id, but is surfaced as unavailable in the flag description
724
- * instead of in `options`, so `--help` never advertises a value that is
725
- * guaranteed to fail and an explicit pass is rejected at parse time — before
726
- * any remote project is created.
727
- */
728
- const UNAVAILABLE_RENDERER_IDS = RENDERER_IDS.filter((id) => !AVAILABLE_RENDERER_IDS.includes(id));
729
- const RENDERER_FLAG_DESCRIPTION = UNAVAILABLE_RENDERER_IDS.length === 0 ? "Renderer (default: react)." : `Renderer (default: react). Not yet available: ${UNAVAILABLE_RENDERER_IDS.join(", ")}.`;
730
- /** `zitadel setup` — create a project and scaffold local auth.
731
- *
732
- * Detects (or, for an empty directory, scaffolds then re-detects) the
733
- * framework, runs the wizard prompts to fill in any answers not pre-supplied
734
- * by flags, creates the remote project without server fallback defaults,
735
- * patches the local files via `Orca`'s framework patcher, then scaffolds and
736
- * uploads editable schema/flow config from `.zitadel/**`.
737
- *
738
- * Every interactive question lives in {@link SETUP_PROMPTS} (the main wizard
739
- * — each entry is a small class) and {@link PickFrameworkPrompt} (the
740
- * empty-directory framework choice, before the main wizard).
741
- */
742
- var Setup = class Setup extends BaseCommand {
743
- static description = "Create a Zitadel project and scaffold local auth.";
744
- static examples = ["<%= config.bin %> setup --framework next", "<%= config.bin %> setup --framework react --dev-port 3000"];
745
- static flags = {
746
- framework: Flags.string({
747
- description: "Framework to target.",
748
- options: FRAMEWORK_OPTIONS
749
- }),
750
- renderer: Flags.string({
751
- description: RENDERER_FLAG_DESCRIPTION,
752
- options: [...AVAILABLE_RENDERER_IDS]
753
- }),
754
- "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." }),
755
- "skip-install": Flags.boolean({ description: "Do not install dependencies after setup updates package.json." }),
756
- preset: Flags.string({
757
- description: "Sign-in preset for the scaffolded schema and login flow (default: password-first).",
758
- options: [...SETUP_PRESETS]
759
- }),
760
- "use-case": Flags.string({
761
- description: "Use case for the scaffolded schema fields: who signs in to the app (default: minimal).",
762
- options: [...SETUP_USE_CASES]
763
- }),
764
- design: Flags.string({
765
- 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).",
766
- options: [...BRANDING_DESIGNS]
767
- })
768
- };
769
- async run() {
770
- const { flags } = await this.parse(Setup);
771
- try {
772
- await this.toMeta(flags);
773
- } catch (error) {
774
- throw localSetupHint(error, retryOptionsFromFlags(flags), this.config.version);
775
- }
776
- const { cwd, nonInteractive, dryRun, force } = this.meta;
777
- if (await hasZitadelConfig(cwd)) return this.emit({
778
- status: "skipped",
779
- reason: "already-initialized"
780
- });
781
- 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." });
782
- const orca = createOrca();
783
- consola$1.start(`Detecting framework in ${shortPath(cwd)}`);
784
- let framework;
785
- let scaffoldedFramework = false;
786
- try {
787
- framework = await orca.detect(cwd, flags.framework);
788
- consola$1.success(`Detected ${framework.id}${framework.devPort ? ` (dev port ${framework.devPort})` : ""}`);
789
- } catch (error) {
790
- if (error instanceof ZitadelError && error.code === "E_FRAMEWORK_NOT_DETECTED") {
791
- const target = await inspectScaffoldTarget(cwd);
792
- if (!target.scaffoldable) throw frameworkDetectionWithScaffoldTarget(error, cwd, target);
793
- consola$1.info("Fresh app directory — scaffolding a fresh project");
794
- framework = await orca.scaffold(cwd, await resolveScaffoldFramework(flags.framework, nonInteractive, orca));
795
- scaffoldedFramework = true;
796
- consola$1.success(`Scaffolded ${framework.id} skeleton`);
797
- } else throw error;
798
- }
799
- this.recordTelemetry({
800
- framework: framework.id,
801
- renderer: flags.renderer ?? "react",
802
- scaffolded_skeleton: scaffoldedFramework,
803
- skip_install: Boolean(flags["skip-install"]),
804
- dev_port_explicit: flags["dev-port"] !== void 0,
805
- preset: flags.preset ?? DEFAULT_SETUP_PRESET,
806
- use_case: flags["use-case"] ?? DEFAULT_SETUP_USE_CASE,
807
- design: flags.design ?? "built-in",
808
- step: "framework_resolved"
809
- });
810
- if (flags["dev-port"] !== void 0) {
811
- const devPort = flags["dev-port"];
812
- if (!Number.isInteger(devPort) || devPort < 1 || devPort > 65535) throw new ZitadelError("E_VALIDATION", `--dev-port must be an integer in 1..65535, got ${devPort}`);
813
- framework = {
814
- ...framework,
815
- devPort,
816
- url: issuerFromPort(devPort)
817
- };
818
- }
819
- let answers = {
820
- server: this.meta.source,
821
- devPort: framework.devPort,
822
- preset: flags.preset ?? DEFAULT_SETUP_PRESET,
823
- useCase: flags["use-case"] ?? DEFAULT_SETUP_USE_CASE,
824
- design: flags.design
825
- };
826
- if (!nonInteractive && !dryRun) {
827
- intro("Zitadel setup");
828
- const promptCtx = {
829
- framework,
830
- cwd,
831
- serverFlag: this.meta.serverFlag,
832
- devPortFromFlag: flags["dev-port"] !== void 0,
833
- presetFromFlag: flags.preset !== void 0,
834
- useCaseFromFlag: flags["use-case"] !== void 0,
835
- designFromFlag: flags.design !== void 0
836
- };
837
- for (const prompt of SETUP_PROMPTS) answers = await prompt.ask(answers, promptCtx);
838
- outro("Configuration captured");
839
- }
840
- this.recordTelemetry({
841
- preset: answers.preset,
842
- use_case: answers.useCase,
843
- design: answers.design ?? "built-in"
844
- });
845
- const issuer = issuerFromPort(answers.devPort);
846
- framework = {
847
- ...framework,
848
- devPort: answers.devPort,
849
- url: issuer
850
- };
851
- consola$1.start(`Creating project on ${answers.server}${dryRun ? " (dry run)" : ""}`);
852
- const unauthClient = createZitadelClient({ baseUrl: answers.server });
853
- const projectName = defaultProjectName(cwd, framework.id);
854
- const project = dryRun ? dryRunProject(issuer) : await createProjectWithLocalHint(unauthClient, answers.server, this.meta.cliVersion, projectName, issuer, {
855
- ...retryOptionsFromFlags(flags),
856
- framework: framework.id,
857
- preset: answers.preset,
858
- useCase: answers.useCase,
859
- design: answers.design,
860
- devPort: answers.devPort
861
- });
862
- consola$1.success(`Created project ${project.id}`);
863
- this.recordTelemetry({ step: "project_created" });
864
- const posture = derivePosture(framework.id, scaffoldedFramework);
865
- const ctx = {
866
- framework,
867
- rendererId: flags.renderer ?? "react",
868
- project,
869
- issuer,
870
- server: answers.server,
871
- cliVersion: this.meta.cliVersion,
872
- scaffoldedFramework,
873
- posture,
874
- preset: answers.preset,
875
- useCase: answers.useCase
876
- };
877
- consola$1.start(`Patching project files${dryRun ? " (dry run)" : ""}`);
878
- const result = await orca.patcherFor(framework.id).patch(ctx, {
879
- cwd,
880
- dryRun,
881
- force
882
- });
883
- for (const file of result.filesWritten) {
884
- const sentence = describeWrittenFile(relativeDisplayPath(cwd, file), dryRun);
885
- if (sentence) consola$1.info(sentence);
886
- }
887
- for (const file of result.filesSkipped) consola$1.info(`Left ${relativeDisplayPath(cwd, file)} unchanged (already matches target)`);
888
- let resourceResult;
889
- try {
890
- resourceResult = dryRun ? { filesWritten: [] } : await materializeSetupResources({
891
- cwd,
892
- client: createZitadelClient({
893
- baseUrl: answers.server,
894
- token: project.project_secret
895
- }),
896
- projectId: project.id,
897
- force,
898
- preset: answers.preset,
899
- useCase: answers.useCase,
900
- design: answers.design,
901
- cliVersion: this.meta.cliVersion
902
- });
903
- } catch (error) {
904
- await rm(join(cwd, "zitadel.json"), { force: true });
905
- await rm(join(cwd, ".zitadel/secret"), { force: true });
906
- const cause = toZitadelError(error);
907
- throw new ZitadelError(cause.code, `Default resource setup failed: ${cause.message}`, {
908
- 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).",
909
- nextCommands: ["zitadel setup --force"],
910
- details: cause.details
911
- });
912
- }
913
- for (const file of resourceResult.filesWritten) {
914
- const sentence = describeWrittenFile(relativeDisplayPath(cwd, file), dryRun);
915
- if (sentence) consola$1.info(sentence);
916
- }
917
- const allFilesWritten = [...result.filesWritten, ...resourceResult.filesWritten];
918
- consola$1.success(`Patched ${allFilesWritten.length} file${allFilesWritten.length === 1 ? "" : "s"}` + (result.filesSkipped.length > 0 ? ` (${result.filesSkipped.length} unchanged)` : ""));
919
- this.recordTelemetry({
920
- step: "files_patched",
921
- files_written_count: allFilesWritten.length
922
- });
923
- if (!dryRun) try {
924
- await writeScaffoldManifest({
925
- cwd,
926
- actions: orca.patcherFor(framework.id).artifacts({
927
- framework,
928
- rendererId: ctx.rendererId
929
- }),
930
- written: [...result.filesWritten, ...result.filesSkipped],
931
- scaffoldedFramework,
932
- devPort: answers.devPort,
933
- posture
934
- });
935
- } catch (error) {
936
- consola$1.debug("Failed to record the scaffold manifest", error);
937
- }
938
- const installOutcome = await installDependenciesForSetup({
939
- cliVersion: this.meta.cliVersion,
940
- cwd,
941
- depsAdded: result.depsAdded,
942
- dryRun,
943
- env: this.meta.env,
944
- issuer,
945
- json: this.jsonEnabled(),
946
- scaffoldedFramework,
947
- skipInstall: Boolean(flags["skip-install"])
948
- });
949
- this.recordTelemetry({
950
- step: "dependencies_installed",
951
- package_manager: installOutcome.install.package_manager
952
- });
953
- const writtenRel = allFilesWritten.map((file) => relativeDisplayPath(cwd, file));
954
- const deadline = dryRun ? void 0 : claimWindowDeadline(project.created_at);
955
- const claimNudge = claimState({
956
- secret: {},
957
- server: answers.server
958
- }).kind === "detached" || serverKind.value(answers.server) === "local" && (dryRun || await localServerHostsPlatform(answers.server)) ? {
959
- actions: [claimAction(this.meta.cliVersion, deadline)],
960
- boxActions: [claimBoxAction(this.meta.cliVersion, deadline)],
961
- commands: [claimCommand(this.meta.cliVersion)]
962
- } : {
963
- actions: [],
964
- boxActions: [],
965
- commands: []
966
- };
967
- const warnings = designWarnings(answers.design);
968
- if (!this.jsonEnabled()) {
969
- const sections = buildSummary({
970
- projectFacts: await detectProjectFacts(cwd, framework.id),
971
- writtenRel,
972
- project,
973
- server: answers.server,
974
- issuer,
975
- scaffoldedFramework,
976
- design: answers.design
977
- });
978
- consola$1.box({
979
- title: "Zitadel is ready",
980
- message: wrapForBox([
981
- renderSummary(sections),
982
- "",
983
- renderBoxActions([...installOutcome.boxActions, ...claimNudge.boxActions])
984
- ].join("\n")),
985
- style: {
986
- padding: 1,
987
- borderStyle: "rounded",
988
- borderColor: "green"
989
- }
990
- });
991
- for (const warning of warnings) consola$1.warn(warning);
992
- }
993
- return this.emit({
994
- status: "ok",
995
- warnings,
996
- pretty: "",
997
- data: {
998
- title: "Zitadel is ready.",
999
- project: {
1000
- project_id: project.id,
1001
- issuer
1002
- },
1003
- framework: framework.id,
1004
- server: answers.server,
1005
- files_written: allFilesWritten.map((file) => relativeDisplayPath(cwd, file)),
1006
- files: result.files.map((file) => ({
1007
- path: relativeDisplayPath(cwd, file.path),
1008
- kind: file.kind,
1009
- action: file.action
1010
- })),
1011
- files_skipped: result.filesSkipped.map((file) => relativeDisplayPath(cwd, file)),
1012
- install: installOutcome.install,
1013
- design: answers.design ?? null,
1014
- next_actions: [
1015
- ...installOutcome.nextActions,
1016
- brandingGuidanceAction(answers.design, this.meta.cliVersion),
1017
- ...claimNudge.actions
1018
- ],
1019
- next_commands: [...installOutcome.nextCommands, ...claimNudge.commands]
1020
- }
1021
- });
1022
- }
1023
- };
1024
- function frameworkDetectionWithScaffoldTarget(error, cwd, target) {
1025
- return new ZitadelError(error.code, "Could not detect a supported app framework, and this directory is not a fresh scaffold target", {
1026
- 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.`,
1027
- details: {
1028
- cwd,
1029
- entries: target.entries,
1030
- reason: target.reason
1031
- }
1032
- });
1033
- }
1034
- /**
1035
- * Resolves which framework to scaffold into an empty directory: the explicit
1036
- * `--framework`, else PickFrameworkPrompt, else a hard error in non-interactive
1037
- * mode (an agent must pass `--framework`).
1038
- */
1039
- async function resolveScaffoldFramework(framework, nonInteractive, orca) {
1040
- if (framework) return framework;
1041
- 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." });
1042
- return new PickFrameworkPrompt().ask(orca.availableFrameworks());
1043
- }
1044
- /**
1045
- * Whether a local server hosts the platform plane, i.e. whether a claim
1046
- * started against it can actually complete. `platform.bootstrap_project`
1047
- * pins the deployment's default project to the well-known proj_platform, and
1048
- * the console runtime document publishes that resolution, so one public GET
1049
- * answers the question. Fail closed: an unreadable, absent, or hanging
1050
- * document means no nudge, never a nudge into a flow that would 401 at
1051
- * claim/complete — the timeout mirrors checkLocalServerHealth so a socket
1052
- * that accepts and stalls cannot hang setup after the real work is done.
1053
- * Exported so the fail-closed behavior is testable without a live server.
1054
- */
1055
- async function localServerHostsPlatform(server, timeoutMs = 1500) {
1056
- try {
1057
- const res = await fetch(new URL("/console/runtime.json", server), { signal: AbortSignal.timeout(timeoutMs) });
1058
- if (!res.ok) return false;
1059
- return (await res.json()).console_project_id === "proj_platform";
1060
- } catch {
1061
- return false;
1062
- }
1063
- }
1064
- /** A deterministic stand-in project for `--dry-run`, so no remote call is made. */
1065
- function dryRunProject(issuer) {
1066
- return {
1067
- id: "dry-run-0000",
1068
- project_secret: "sk_proj_dry_run_full",
1069
- preview_secret: "sk_proj_dry_run_preview",
1070
- preview_origins: [issuer],
1071
- created_at: "2026-04-21T14:03:11.000Z"
1072
- };
1073
- }
1074
- /**
1075
- * Reconstructs the flag list of the current invocation for retry guidance,
1076
- * ending in `--server local`. Flags whose resolved value equals the default
1077
- * are omitted — the retry reproduces the same outcome without them.
1078
- */
1079
- function setupRetryFlags(opts) {
1080
- const parts = [];
1081
- if (opts.framework) parts.push(`--framework ${opts.framework}`);
1082
- if (opts.preset && opts.preset !== DEFAULT_SETUP_PRESET) parts.push(`--preset ${opts.preset}`);
1083
- if (opts.useCase && opts.useCase !== DEFAULT_SETUP_USE_CASE) parts.push(`--use-case ${opts.useCase}`);
1084
- if (opts.design) parts.push(`--design ${opts.design}`);
1085
- if (opts.renderer && opts.renderer !== "react") parts.push(`--renderer ${opts.renderer}`);
1086
- if (opts.devPort !== void 0) parts.push(`--dev-port ${opts.devPort}`);
1087
- if (opts.nonInteractive) parts.push("--non-interactive");
1088
- parts.push("--server local");
1089
- return parts.join(" ");
1090
- }
1091
- /**
1092
- * Retry options straight from parsed flags, for failures before the wizard
1093
- * resolves answers. `--non-interactive` is echoed only when explicitly passed
1094
- * — TTY/JSON-inferred non-interactivity re-infers itself on the retry.
1095
- */
1096
- function retryOptionsFromFlags(flags) {
1097
- return {
1098
- framework: flags.framework,
1099
- preset: flags.preset,
1100
- useCase: flags["use-case"],
1101
- design: flags.design,
1102
- renderer: flags.renderer,
1103
- devPort: flags["dev-port"],
1104
- nonInteractive: Boolean(flags["non-interactive"])
1105
- };
1106
- }
1107
- async function createProjectWithLocalHint(client, server, cliVersion, projectName, issuer, retry) {
1108
- try {
1109
- const payload = {
1110
- name: projectName,
1111
- preview_origins: [issuer],
1112
- seed_defaults: false
1113
- };
1114
- return await client.createProject(payload);
1115
- } catch (error) {
1116
- const normalized = toZitadelError(error);
1117
- const retryFlags = setupRetryFlags(retry);
1118
- throw new ZitadelError(normalized.code, normalized.message, {
1119
- hint: `${normalized.hint ? `${normalized.hint} ` : ""}If you meant to use a local Zitadel server, start it first and retry setup with ${retryFlags}.`,
1120
- nextCommands: [publicCliCommand("start", cliVersion), publicCliCommand(`setup ${retryFlags}`, cliVersion)],
1121
- details: {
1122
- server,
1123
- original: normalized.details
1124
- }
1125
- });
1126
- }
1127
- }
1128
- function defaultProjectName(cwd, framework) {
1129
- const fromDirectory = basename(cwd).trim();
1130
- return fromDirectory.length > 0 ? fromDirectory : `zitadel-${framework}-app`;
1131
- }
1132
- function localSetupHint(error, retry, cliVersion) {
1133
- const normalized = toZitadelError(error);
1134
- if (normalized.code !== "E_LOCAL_SERVER_NOT_RUNNING") return error;
1135
- const setupCommand = `setup ${setupRetryFlags(retry)}`;
1136
- return new ZitadelError(normalized.code, normalized.message, {
1137
- 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.`,
1138
- nextCommands: [publicCliCommand("start", cliVersion), publicCliCommand(setupCommand, cliVersion)],
1139
- details: normalized.details
1140
- });
1141
- }
1142
- /**
1143
- * Replaces the user's `$HOME` with `~` in a path for compact terminal output.
1144
- * Falls back to the raw path when `HOME` isn't set or doesn't match.
1145
- */
1146
- function shortPath(absolute) {
1147
- const home = process.env.HOME;
1148
- if (home && absolute.startsWith(home)) return `~${absolute.slice(home.length)}`;
1149
- return absolute;
1150
- }
1151
- /**
1152
- * Picks one written file by suffix so the corresponding INSTALLED row can
1153
- * reference it without hard-coding the path the patcher chose. Returns
1154
- * the first match; falls back to `undefined` when the patcher didn't
1155
- * write that artifact (e.g. a renderer without a register page).
1156
- */
1157
- function pickWrittenFile(written, suffix) {
1158
- return written.find((file) => file.endsWith(suffix));
1159
- }
1160
- /**
1161
- * Translates a patcher-written path into a single sentence the user can
1162
- * read at narration speed. The patch result's `filesWritten` carries
1163
- * deduplicated file paths only (directories stay in the typed `files`
1164
- * rows), so no artefact filtering is needed here. The verb tense flips
1165
- * for `--dry-run` so the user sees a preview ("Would write ...") instead
1166
- * of a claim that something happened.
1167
- */
1168
- function describeWrittenFile(relPath, dryRun) {
1169
- const verb = dryRun ? "Would write" : "Wrote";
1170
- const sentence = SENTENCE_BY_PATH[relPath];
1171
- if (sentence) return `${verb} ${sentence.subject} (${path(relPath)})`;
1172
- return `${verb} ${path(relPath)}`;
1173
- }
1174
- /**
1175
- * Map from the patcher's deterministic output paths to a short noun
1176
- * phrase describing what the file is for. Anything not in the map falls
1177
- * back to the bare path in the narration; add an entry here when a new
1178
- * scaffolded file deserves a clearer label.
1179
- */
1180
- const SENTENCE_BY_PATH = {
1181
- ".gitignore": { subject: "the project's .gitignore additions" },
1182
- ".zitadel/secret": { subject: "the local project secret" },
1183
- "zitadel.json": { subject: "the Zitadel project configuration" },
1184
- ".env.example": { subject: "the .env example template" },
1185
- ".env.local": { subject: "the local development environment variables" },
1186
- ".zitadel/state.json": { subject: "the sync state file" },
1187
- ".zitadel/flows/default-login.json": { subject: "the editable default login flow" },
1188
- ".zitadel/flows/README.md": { subject: "the flows folder README" },
1189
- ".zitadel/schemas/default-human-user.json": { subject: "the editable default human user schema" },
1190
- ".zitadel/schemas/README.md": { subject: "the schemas folder README" },
1191
- ".zitadel/meta/flow-definition.json": { subject: "the flow dialect spec (editor $schema)" },
1192
- ".zitadel/meta/user-schema.json": { subject: "the user-schema dialect spec" },
1193
- ".zitadel/meta/user-property.json": { subject: "the user-property dialect spec" },
1194
- ".zitadel/meta/branding.json": { subject: "the branding dialect spec" },
1195
- ".zitadel/branding/branding.json": { subject: "the branding descriptor (layout + asset URLs)" },
1196
- ".zitadel/branding/login.liquid": { subject: "the editable login template" },
1197
- ".zitadel/branding/README.md": { subject: "the branding folder README" },
1198
- "AGENTS.md": { subject: "the agent guidance (golden journey + config dialect)" },
1199
- "README.md": { subject: "the README's Zitadel section" },
1200
- "app/page.tsx": { subject: "the home page redirect" },
1201
- "app/login/page.tsx": { subject: "the login page" },
1202
- "app/register/page.tsx": { subject: "the registration page" },
1203
- "app/profile/page.tsx": { subject: "the profile page" },
1204
- "middleware.ts": { subject: "the Next.js middleware" },
1205
- "proxy.ts": { subject: "the Next.js proxy" },
1206
- "custom-elements.d.ts": { subject: "the web-component type declarations" },
1207
- "package.json": { subject: "package.json with the SDK dependency" }
1208
- };
1209
- /** Builds the section list driving {@link renderSummary} for the setup command. */
1210
- function buildSummary(opts) {
1211
- const { projectFacts, writtenRel, project, server, issuer, scaffoldedFramework, design } = opts;
1212
- const sdkPackage = "@zitadel/sdk-next";
1213
- const packageJsonHit = pickWrittenFile(writtenRel, "package.json");
1214
- const detected = [{
1215
- label: "Framework",
1216
- value: formatFrameworkLine(projectFacts)
1217
- }];
1218
- if (scaffoldedFramework) detected.push({
1219
- label: "Scaffold",
1220
- value: "fresh project (no existing files)"
1221
- });
1222
- const installedRows = [];
1223
- if (packageJsonHit) installedRows.push({
1224
- label: "Package",
1225
- value: sdkPackage,
1226
- secondary: path(fileNameOf(packageJsonHit))
1227
- });
1228
- for (const [label, suffix] of [
1229
- ["Home redirect", "app/page.tsx"],
1230
- ["Login page", "app/login/page.tsx"],
1231
- ["Register page", "app/register/page.tsx"],
1232
- ["Profile page", "app/profile/page.tsx"],
1233
- ["Request proxy", "proxy.ts"],
1234
- ["Middleware", "middleware.ts"],
1235
- ["Env vars", ".env.local"]
1236
- ]) {
1237
- const hit = pickWrittenFile(writtenRel, suffix);
1238
- if (hit) installedRows.push({
1239
- label,
1240
- value: path(hit)
1241
- });
1242
- }
1243
- const customizeRows = [];
1244
- for (const [label, suffix, dir] of [
1245
- [
1246
- "User schema",
1247
- ".zitadel/schemas/default-human-user.json",
1248
- ".zitadel/schemas/"
1249
- ],
1250
- [
1251
- "Login flow",
1252
- ".zitadel/flows/default-login.json",
1253
- ".zitadel/flows/"
1254
- ],
1255
- [
1256
- "Login template",
1257
- ".zitadel/branding/login.liquid",
1258
- ".zitadel/branding/"
1259
- ]
1260
- ]) if (pickWrittenFile(writtenRel, suffix)) customizeRows.push({
1261
- label,
1262
- value: path(dir),
1263
- secondary: "see its README.md"
1264
- });
1265
- const projectRows = [
1266
- {
1267
- label: "Project id",
1268
- value: id(project.id)
1269
- },
1270
- {
1271
- label: "Server",
1272
- value: url(server)
1273
- },
1274
- {
1275
- label: "App will run",
1276
- value: url(issuer)
1277
- },
1278
- design ? {
1279
- label: "Login design",
1280
- value: brandingDesignLabel(design),
1281
- secondary: `${design} · ${path(".zitadel/branding/")}`
1282
- } : {
1283
- label: "Login design",
1284
- value: dim("built-in template")
1285
- }
1286
- ];
1287
- return [
1288
- {
1289
- title: "Detected",
1290
- rows: detected
1291
- },
1292
- {
1293
- title: "Installed",
1294
- rows: installedRows
1295
- },
1296
- {
1297
- title: "Customize",
1298
- rows: customizeRows
1299
- },
1300
- {
1301
- title: "Project",
1302
- rows: projectRows
1303
- }
1304
- ];
1305
- }
1306
- //#endregion
1307
- export { Setup as default, localServerHostsPlatform };
1308
-
1309
- //# sourceMappingURL=setup.mjs.map