@zitadel/cli 0.1.0-alpha.16 → 0.1.0-alpha.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -107
- package/SKILLS.md +98 -13
- package/dist/branding-BXj028-W.mjs +79 -0
- package/dist/branding-BXj028-W.mjs.map +1 -0
- package/dist/claim-state-BTGInAaN.mjs +66 -0
- package/dist/claim-state-BTGInAaN.mjs.map +1 -0
- package/dist/commands/apply.mjs +12 -7
- package/dist/commands/apply.mjs.map +1 -1
- package/dist/commands/branding/eject.mjs +125 -0
- package/dist/commands/branding/eject.mjs.map +1 -0
- package/dist/commands/claim.mjs +402 -0
- package/dist/commands/claim.mjs.map +1 -0
- package/dist/commands/doctor.mjs +477 -38
- package/dist/commands/doctor.mjs.map +1 -1
- package/dist/commands/eject.mjs +3 -3
- package/dist/commands/logs.mjs +2 -2
- package/dist/commands/plan.mjs +7 -5
- package/dist/commands/plan.mjs.map +1 -1
- package/dist/commands/reset.mjs +2 -2
- package/dist/commands/schemas/list.mjs +14 -10
- package/dist/commands/schemas/list.mjs.map +1 -1
- package/dist/commands/setup.mjs +331 -120
- package/dist/commands/setup.mjs.map +1 -1
- package/dist/commands/start.mjs +3 -4
- package/dist/commands/start.mjs.map +1 -1
- package/dist/commands/status.mjs +21 -13
- package/dist/commands/status.mjs.map +1 -1
- package/dist/commands/stop.mjs +3 -3
- package/dist/commands/stop.mjs.map +1 -1
- package/dist/{docker-BbX0yOpg.mjs → docker-Ciz9N6uI.mjs} +2 -2
- package/dist/{docker-BbX0yOpg.mjs.map → docker-Ciz9N6uI.mjs.map} +1 -1
- package/dist/{environment-ABj1IWZt.mjs → environment-CuHKxtIO.mjs} +1 -1
- package/dist/{environment-ABj1IWZt.mjs.map → environment-CuHKxtIO.mjs.map} +1 -1
- package/dist/{journey-guidance-E_3_UwGt.mjs → journey-guidance-CtQUfWar.mjs} +14 -3
- package/dist/{journey-guidance-E_3_UwGt.mjs.map → journey-guidance-CtQUfWar.mjs.map} +1 -1
- package/dist/{oclif-SjHLB_XK.mjs → oclif-CL6-M13m.mjs} +220 -43
- package/dist/oclif-CL6-M13m.mjs.map +1 -0
- package/dist/{orca-_TmrJQMw.mjs → orca-BWB4zOdw.mjs} +749 -192
- package/dist/orca-BWB4zOdw.mjs.map +1 -0
- package/dist/{ports-XujDtA47.mjs → ports-Cu2Y7aXQ.mjs} +25 -20
- package/dist/ports-Cu2Y7aXQ.mjs.map +1 -0
- package/dist/{processes-p2PPbKGG.mjs → processes-Bn2WngOi.mjs} +1 -1
- package/dist/{processes-p2PPbKGG.mjs.map → processes-Bn2WngOi.mjs.map} +1 -1
- package/dist/{project-DXc7q4wN.mjs → project-yejJSfdJ.mjs} +57 -4
- package/dist/project-yejJSfdJ.mjs.map +1 -0
- package/dist/scaffold-manifest-CKNFLX9T.mjs +81 -0
- package/dist/scaffold-manifest-CKNFLX9T.mjs.map +1 -0
- package/dist/{sync-Bu_iLIbb.mjs → sync-Cx2_4rXW.mjs} +178 -52
- package/dist/sync-Cx2_4rXW.mjs.map +1 -0
- package/dist/user-schema-W1OsXdpc.mjs +91 -0
- package/dist/user-schema-W1OsXdpc.mjs.map +1 -0
- package/oclif.manifest.json +227 -5
- package/package.json +7 -5
- package/dist/docker-guidance-3UZESTfb.mjs +0 -21
- package/dist/docker-guidance-3UZESTfb.mjs.map +0 -1
- package/dist/oclif-SjHLB_XK.mjs.map +0 -1
- package/dist/orca-_TmrJQMw.mjs.map +0 -1
- package/dist/ports-XujDtA47.mjs.map +0 -1
- package/dist/project-DXc7q4wN.mjs.map +0 -1
- package/dist/sync-Bu_iLIbb.mjs.map +0 -1
- package/dist/user-schema-DDz5-lX5.mjs +0 -13
- package/dist/user-schema-DDz5-lX5.mjs.map +0 -1
package/dist/commands/setup.mjs
CHANGED
|
@@ -1,20 +1,51 @@
|
|
|
1
|
-
import { D as toZitadelError, E as ZitadelError, T as stableStringify,
|
|
2
|
-
import { n as
|
|
3
|
-
import { n as
|
|
4
|
-
import {
|
|
5
|
-
import { t as
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
1
|
+
import { D as toZitadelError, E as ZitadelError, T as stableStringify, k as publicCliCommand, n as DEFAULT_SERVER, t as BaseCommand, u as detectHealthyLocalServer } from "../oclif-CL6-M13m.mjs";
|
|
2
|
+
import { n as claimCommand, r as claimState, t as claimAction } from "../claim-state-BTGInAaN.mjs";
|
|
3
|
+
import { n as customizeAndPublishActions, r as verifyLoginAction, t as brandingGuidanceAction } from "../journey-guidance-CtQUfWar.mjs";
|
|
4
|
+
import { i as RENDERER_IDS, n as inspectScaffoldTarget, r as AVAILABLE_RENDERER_IDS, s as issuerFromPort, t as createOrca } from "../orca-BWB4zOdw.mjs";
|
|
5
|
+
import { n as hasZitadelSecret, t as hasZitadelConfig } from "../project-yejJSfdJ.mjs";
|
|
6
|
+
import { a as updateState, t as SCHEMAS_DIR } from "../user-schema-W1OsXdpc.mjs";
|
|
7
|
+
import { r as writeScaffoldManifest } from "../scaffold-manifest-CKNFLX9T.mjs";
|
|
8
|
+
import { i as toBrandingWireBody, t as BRANDING_DIR } from "../branding-BXj028-W.mjs";
|
|
9
|
+
import { c as writeBackResource, o as hashForState, u as FLOWS_DIR } from "../sync-Cx2_4rXW.mjs";
|
|
8
10
|
import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
9
11
|
import { basename, dirname, join } from "node:path";
|
|
10
12
|
import { cancel, confirm, intro, isCancel, outro, select, spinner, text } from "@clack/prompts";
|
|
11
13
|
import { Flags } from "@oclif/core";
|
|
12
14
|
import { createZitadelClient } from "@zitadel/api/client";
|
|
13
|
-
import { DEFAULT_FLOW_CONFIG_PATH, DEFAULT_FLOW_SCHEMA_URI, DEFAULT_SCHEMA_CONFIG_PATH, DEFAULT_SETUP_PRESET, SETUP_PRESETS, flowsReadmeContent, getDefaultHumanUserSchema, getDefaultLoginFlow, schemasReadmeContent } from "@zitadel/config/defaults";
|
|
15
|
+
import { BRANDING_DESIGNS, DEFAULT_BRANDING_CONFIG_PATH, DEFAULT_BRANDING_TEMPLATE_PATH, DEFAULT_FLOW_CONFIG_PATH, DEFAULT_FLOW_SCHEMA_URI, DEFAULT_SCHEMA_CONFIG_PATH, DEFAULT_SETUP_PRESET, DEFAULT_SETUP_USE_CASE, SETUP_PRESETS, SETUP_USE_CASES, brandingReadmeContent, flowsReadmeContent, getDefaultBrandingConfig, getDefaultHumanUserSchema, getDefaultLoginFlow, schemasReadmeContent } from "@zitadel/config/defaults";
|
|
14
16
|
import { consola as consola$1 } from "consola";
|
|
17
|
+
import { BRANDING_FILE_SCHEMA_REF } from "@zitadel/config/meta-schemas";
|
|
15
18
|
import { spawn } from "node:child_process";
|
|
16
19
|
import { normalizeFlowBody, normalizeSchemaBody } from "@zitadel/config/normalize";
|
|
17
20
|
import pc from "picocolors";
|
|
21
|
+
//#region src/lib/orca/patchers/posture.ts
|
|
22
|
+
/**
|
|
23
|
+
* Frameworks whose patchers add route files without owning the app shell —
|
|
24
|
+
* the only ones where a pre-existing app keeps a layout for the widget
|
|
25
|
+
* posture to inherit (ADR 044). The SPA families write the app's root
|
|
26
|
+
* component, so nothing survives for a widget to embed into; they keep the
|
|
27
|
+
* page posture until a non-destructive route/layout insertion contract
|
|
28
|
+
* exists for their routers.
|
|
29
|
+
*/
|
|
30
|
+
const ROUTE_BASED_FRAMEWORKS = new Set(["next", "nuxt"]);
|
|
31
|
+
/**
|
|
32
|
+
* The default embedding posture of the scaffolded auth/profile pages
|
|
33
|
+
* (ADR 044), derived from the same hinge as the framework homepage: whether
|
|
34
|
+
* setup created the app skeleton itself. A fresh scaffold has no design to
|
|
35
|
+
* respect — full-page chrome is the strongest start. A pre-existing
|
|
36
|
+
* route-based app has its own shell and theme, so the pages embed
|
|
37
|
+
* `variant="widget"` cards in a layout-neutral wrapper instead of painting
|
|
38
|
+
* token-colored chrome underneath the host layout.
|
|
39
|
+
*
|
|
40
|
+
* Derived once at setup time and recorded in the scaffold manifest;
|
|
41
|
+
* `doctor --fix` restores from the record rather than re-deriving (a
|
|
42
|
+
* manifest-less legacy scaffold could not answer the hinge).
|
|
43
|
+
*/
|
|
44
|
+
function derivePosture(frameworkId, scaffoldedFramework) {
|
|
45
|
+
if (!ROUTE_BASED_FRAMEWORKS.has(frameworkId)) return "page";
|
|
46
|
+
return scaffoldedFramework ? "page" : "widget";
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
18
49
|
//#region src/lib/setup-resources.ts
|
|
19
50
|
/**
|
|
20
51
|
* Scaffolds the versioned local default resources for a new project, uploads
|
|
@@ -32,7 +63,11 @@ async function materializeSetupResources(opts) {
|
|
|
32
63
|
await mkdir(join(opts.cwd, SCHEMAS_DIR), { recursive: true });
|
|
33
64
|
const filesWritten = [];
|
|
34
65
|
const preset = opts.preset ?? DEFAULT_SETUP_PRESET;
|
|
35
|
-
const
|
|
66
|
+
const useCase = opts.useCase ?? DEFAULT_SETUP_USE_CASE;
|
|
67
|
+
const { $id: _templateId, ...schemaBody } = getDefaultHumanUserSchema({
|
|
68
|
+
preset,
|
|
69
|
+
useCase
|
|
70
|
+
});
|
|
36
71
|
if (await writeResourceFile(opts.cwd, DEFAULT_SCHEMA_CONFIG_PATH, schemaBody, opts.force)) filesWritten.push(join(opts.cwd, DEFAULT_SCHEMA_CONFIG_PATH));
|
|
37
72
|
const schemaId = requiredString((await opts.client.createSchema(schemaBody, { project_id: opts.projectId })).id, "created schema id");
|
|
38
73
|
let schemaHash = hashForState({ normalize: normalizeSchemaBody }, schemaBody);
|
|
@@ -48,7 +83,8 @@ async function materializeSetupResources(opts) {
|
|
|
48
83
|
});
|
|
49
84
|
const flowBody = getDefaultLoginFlow({
|
|
50
85
|
userSchemaUrl: schemaId,
|
|
51
|
-
preset
|
|
86
|
+
preset,
|
|
87
|
+
useCase
|
|
52
88
|
});
|
|
53
89
|
if (await writeResourceFile(opts.cwd, DEFAULT_FLOW_CONFIG_PATH, flowBody, opts.force)) filesWritten.push(join(opts.cwd, DEFAULT_FLOW_CONFIG_PATH));
|
|
54
90
|
const flow = await opts.client.createFlowDefinition({
|
|
@@ -67,6 +103,24 @@ async function materializeSetupResources(opts) {
|
|
|
67
103
|
name: flowBody.name,
|
|
68
104
|
status: flowBody.status
|
|
69
105
|
});
|
|
106
|
+
if (opts.design) {
|
|
107
|
+
await mkdir(join(opts.cwd, BRANDING_DIR), { recursive: true });
|
|
108
|
+
const { branding, template } = getDefaultBrandingConfig(opts.design);
|
|
109
|
+
const descriptor = {
|
|
110
|
+
$schema: BRANDING_FILE_SCHEMA_REF,
|
|
111
|
+
...branding
|
|
112
|
+
};
|
|
113
|
+
if (await writeResourceFile(opts.cwd, DEFAULT_BRANDING_CONFIG_PATH, descriptor, opts.force)) filesWritten.push(join(opts.cwd, DEFAULT_BRANDING_CONFIG_PATH));
|
|
114
|
+
if (await writeRawFile(opts.cwd, DEFAULT_BRANDING_TEMPLATE_PATH, template, opts.force)) filesWritten.push(join(opts.cwd, DEFAULT_BRANDING_TEMPLATE_PATH));
|
|
115
|
+
const brandingNormalize = (data) => toBrandingWireBody(opts.cwd, data);
|
|
116
|
+
const created = await opts.client.createBranding(brandingNormalize(descriptor), { project_id: opts.projectId });
|
|
117
|
+
await updateState(opts.cwd, DEFAULT_BRANDING_CONFIG_PATH, {
|
|
118
|
+
id: requiredString(created.id, "created branding revision id"),
|
|
119
|
+
hash: hashForState({ normalize: brandingNormalize }, descriptor)
|
|
120
|
+
});
|
|
121
|
+
const brandingReadme = join(BRANDING_DIR, "README.md");
|
|
122
|
+
if (await writeReadmeFile(opts.cwd, brandingReadme, brandingReadmeContent())) filesWritten.push(join(opts.cwd, brandingReadme));
|
|
123
|
+
}
|
|
70
124
|
const schemasReadme = join(SCHEMAS_DIR, "README.md");
|
|
71
125
|
const flowsReadme = join(FLOWS_DIR, "README.md");
|
|
72
126
|
if (await writeReadmeFile(opts.cwd, schemasReadme, schemasReadmeContent())) filesWritten.push(join(opts.cwd, schemasReadme));
|
|
@@ -74,6 +128,22 @@ async function materializeSetupResources(opts) {
|
|
|
74
128
|
return { filesWritten };
|
|
75
129
|
}
|
|
76
130
|
/**
|
|
131
|
+
* Write a non-JSON scaffold file (the `.liquid` template) with the same
|
|
132
|
+
* conflict semantics as {@link writeResourceFile}: `--force` overwrites,
|
|
133
|
+
* otherwise an existing file is an `E_CONFLICT`.
|
|
134
|
+
*/
|
|
135
|
+
async function writeRawFile(cwd, relPath, content, force) {
|
|
136
|
+
const dest = join(cwd, relPath);
|
|
137
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
138
|
+
try {
|
|
139
|
+
await writeFile(dest, content, force ? void 0 : { flag: "wx" });
|
|
140
|
+
return true;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (isErrno(error, "EEXIST")) throw new ZitadelError("E_CONFLICT", `${relPath} already exists`, { hint: "Move the file aside or rerun setup with --force if you want setup to replace it." });
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
77
147
|
* Write a README file, but never overwrite an existing one. A developer who
|
|
78
148
|
* has edited the README should keep their edits when `setup --force` is
|
|
79
149
|
* re-run.
|
|
@@ -295,6 +365,77 @@ function bail(value) {
|
|
|
295
365
|
}
|
|
296
366
|
}
|
|
297
367
|
//#endregion
|
|
368
|
+
//#region src/commands/setup/prompts/design.ts
|
|
369
|
+
/**
|
|
370
|
+
* Sentinel select value for "keep the built-in login" — clack option values
|
|
371
|
+
* must be strings, and `undefined` in {@link SetupAnswers.design} is the
|
|
372
|
+
* real representation of that choice.
|
|
373
|
+
*/
|
|
374
|
+
const BUILT_IN = "built-in";
|
|
375
|
+
/**
|
|
376
|
+
* "How should the login look?" — the last wizard question (#676). Asks after
|
|
377
|
+
* the sign-in preset so the order tells a story: the use case owns what is
|
|
378
|
+
* collected, the preset owns how users authenticate, the design owns how
|
|
379
|
+
* that experience looks.
|
|
380
|
+
*
|
|
381
|
+
* The built-in default is preselected and writes nothing. Picking a starter
|
|
382
|
+
* design forks its Liquid template into `.zitadel/branding/` and publishes
|
|
383
|
+
* it as branding revision 1 — from then on the template is repo-owned and
|
|
384
|
+
* stops tracking built-in improvements, so the option labels make the
|
|
385
|
+
* file-write explicit and keep ownership an opt-in. `--design` is
|
|
386
|
+
* authoritative; non-interactive runs keep the built-in template.
|
|
387
|
+
*/
|
|
388
|
+
var DesignPrompt = class {
|
|
389
|
+
async ask(answers, ctx) {
|
|
390
|
+
if (ctx.designFromFlag) return answers;
|
|
391
|
+
const value = await select({
|
|
392
|
+
message: "How should the login look? A starter design adds its editable template to .zitadel/branding/.",
|
|
393
|
+
initialValue: answers.design ?? BUILT_IN,
|
|
394
|
+
options: [
|
|
395
|
+
{
|
|
396
|
+
value: BUILT_IN,
|
|
397
|
+
label: "Built-in",
|
|
398
|
+
hint: "no files added; pick a design anytime later with `branding eject`"
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
value: "centered",
|
|
402
|
+
label: "Centered",
|
|
403
|
+
hint: "card centred on the page"
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
value: "split",
|
|
407
|
+
label: "Split",
|
|
408
|
+
hint: "brand panel left, form right"
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
value: "split-right",
|
|
412
|
+
label: "Split (reversed)",
|
|
413
|
+
hint: "form left, brand panel right"
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
value: "hero",
|
|
417
|
+
label: "Hero",
|
|
418
|
+
hint: "landing-style brand pane left, form right"
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
value: "minimal",
|
|
422
|
+
label: "Minimal",
|
|
423
|
+
hint: "no card chrome, fields straight on the page"
|
|
424
|
+
}
|
|
425
|
+
]
|
|
426
|
+
});
|
|
427
|
+
bail(value);
|
|
428
|
+
if (value === BUILT_IN) return {
|
|
429
|
+
...answers,
|
|
430
|
+
design: void 0
|
|
431
|
+
};
|
|
432
|
+
return {
|
|
433
|
+
...answers,
|
|
434
|
+
design: value
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
//#endregion
|
|
298
439
|
//#region src/commands/setup/prompts/dev-port.ts
|
|
299
440
|
/**
|
|
300
441
|
* "Dev server port" — defaults to the detected port. The validated answer
|
|
@@ -338,64 +479,27 @@ var FrameworkConfirmPrompt = class {
|
|
|
338
479
|
}
|
|
339
480
|
};
|
|
340
481
|
//#endregion
|
|
341
|
-
//#region src/lib/prober/http.ts
|
|
342
|
-
/**
|
|
343
|
-
* Fetch `url` with a per-call timeout and pass the resulting `Response` to
|
|
344
|
-
* `predicate`. Returns the predicate's value, or `null` on any failure: a
|
|
345
|
-
* network error, an `AbortError` from the timeout, a thrown predicate, or the
|
|
346
|
-
* predicate returning `null`.
|
|
347
|
-
*
|
|
348
|
-
* Never throws. The predicate decides what counts as a match — it gets the
|
|
349
|
-
* raw `Response` and may read `.json()` / `.headers` / `.status` as needed.
|
|
350
|
-
*
|
|
351
|
-
* `timeoutMs` defaults to 500ms. The implementation uses
|
|
352
|
-
* `AbortSignal.timeout`, so the underlying fetch is cancelled when the
|
|
353
|
-
* timeout fires (no orphaned sockets).
|
|
354
|
-
*/
|
|
355
|
-
async function probeUrl(url, predicate, opts) {
|
|
356
|
-
const timeoutMs = opts?.timeoutMs ?? 500;
|
|
357
|
-
try {
|
|
358
|
-
return await predicate(await fetch(url, { signal: AbortSignal.timeout(timeoutMs) }));
|
|
359
|
-
} catch {
|
|
360
|
-
return null;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
/**
|
|
364
|
-
* Probe many URLs in parallel and return the non-null matches, preserving
|
|
365
|
-
* the input order. Each URL is bounded by `timeoutMs` independently — one
|
|
366
|
-
* slow target can't hold up the others.
|
|
367
|
-
*/
|
|
368
|
-
async function probeUrls(urls, predicate, opts) {
|
|
369
|
-
const list = [...urls];
|
|
370
|
-
return (await Promise.all(list.map(async (url) => {
|
|
371
|
-
const value = await probeUrl(url, predicate, opts);
|
|
372
|
-
return value === null ? null : {
|
|
373
|
-
url,
|
|
374
|
-
value
|
|
375
|
-
};
|
|
376
|
-
}))).filter((match) => match !== null);
|
|
377
|
-
}
|
|
378
|
-
//#endregion
|
|
379
482
|
//#region src/commands/setup/prompts/server.ts
|
|
380
483
|
/** Sentinel returned by the choice select when the user picks "Custom URL". */
|
|
381
484
|
const CUSTOM = "__custom__";
|
|
382
|
-
/** Per-probe timeout for the localhost OIDC scan — generous enough for a TLS handshake on a busy laptop. */
|
|
383
|
-
const OIDC_PROBE_TIMEOUT_MS = 300;
|
|
384
485
|
/**
|
|
385
|
-
* "Which server should `zitadel.json` point to?" — Zitadel Cloud,
|
|
386
|
-
*
|
|
486
|
+
* "Which server should `zitadel.json` point to?" — Zitadel Cloud, the managed
|
|
487
|
+
* local server we detected, or a custom URL.
|
|
387
488
|
*
|
|
388
|
-
* Before asking, it
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
489
|
+
* Before asking, it looks for a healthy local Zitadel server the same way the
|
|
490
|
+
* rest of the CLI does (`detectHealthyLocalServer`: the runtime metadata that
|
|
491
|
+
* `zitadel start` wrote to `.zitadel/local/runtime.json`, falling back to a
|
|
492
|
+
* `/healthz` probe on the default localhost port). A detected server becomes
|
|
493
|
+
* an extra option in the choice list *and* the preselected answer — a user
|
|
494
|
+
* who just ran `zitadel start` almost certainly wants it, and `start`'s own
|
|
495
|
+
* next-step hint says `setup --server local`. Picking it writes the URL
|
|
496
|
+
* directly to `answers.server`; picking "Custom URL" still asks for a
|
|
393
497
|
* validated URL exactly as before.
|
|
394
498
|
*/
|
|
395
499
|
var ServerPrompt = class {
|
|
396
500
|
async ask(answers, ctx) {
|
|
397
501
|
if (ctx.serverFlag) return answers;
|
|
398
|
-
const
|
|
502
|
+
const detected = await detectLocalServer(ctx.cwd);
|
|
399
503
|
const choice = await select({
|
|
400
504
|
message: "Which server should zitadel.json point to?",
|
|
401
505
|
options: [
|
|
@@ -404,17 +508,17 @@ var ServerPrompt = class {
|
|
|
404
508
|
label: "Zitadel Cloud (api.zitadel.cloud)",
|
|
405
509
|
hint: "recommended for real projects"
|
|
406
510
|
},
|
|
407
|
-
...
|
|
408
|
-
value:
|
|
409
|
-
label: server
|
|
410
|
-
hint: "detected —
|
|
411
|
-
}
|
|
511
|
+
...detected ? [{
|
|
512
|
+
value: detected,
|
|
513
|
+
label: `Local Zitadel server (${detected})`,
|
|
514
|
+
hint: "detected — started with zitadel start"
|
|
515
|
+
}] : [],
|
|
412
516
|
{
|
|
413
517
|
value: CUSTOM,
|
|
414
518
|
label: "Custom URL (self-hosted)"
|
|
415
519
|
}
|
|
416
520
|
],
|
|
417
|
-
initialValue: answers.server ?? "https://api.zitadel.cloud"
|
|
521
|
+
initialValue: detected ?? answers.server ?? "https://api.zitadel.cloud"
|
|
418
522
|
});
|
|
419
523
|
bail(choice);
|
|
420
524
|
if (choice !== CUSTOM) return {
|
|
@@ -441,37 +545,16 @@ var ServerPrompt = class {
|
|
|
441
545
|
}
|
|
442
546
|
};
|
|
443
547
|
/**
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
* Zitadel-shaped detail here is the OIDC predicate, which is the standard
|
|
448
|
-
* "any OIDC server" contract (no `zitadel`-keyword check), so Keycloak/dex/
|
|
449
|
-
* etc. also surface and the user picks the right one.
|
|
548
|
+
* Wraps {@link detectHealthyLocalServer} in a spinner: the unhealthy paths
|
|
549
|
+
* block on up to two 1.5s `/healthz` probes, long enough that the wizard
|
|
550
|
+
* should say what it is doing.
|
|
450
551
|
*/
|
|
451
|
-
async function
|
|
552
|
+
async function detectLocalServer(cwd) {
|
|
452
553
|
const s = spinner();
|
|
453
|
-
s.start("
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
s.stop("No local servers detected.");
|
|
458
|
-
return [];
|
|
459
|
-
}
|
|
460
|
-
const origins = (await probeUrls(ports.map((port) => `http://localhost:${port}/.well-known/openid-configuration`), async (response) => {
|
|
461
|
-
if (!response.ok) return null;
|
|
462
|
-
try {
|
|
463
|
-
const body = await response.json();
|
|
464
|
-
return typeof body.issuer === "string" ? body.issuer : null;
|
|
465
|
-
} catch {
|
|
466
|
-
return null;
|
|
467
|
-
}
|
|
468
|
-
}, { timeoutMs: OIDC_PROBE_TIMEOUT_MS })).map((match) => new URL(match.url).origin);
|
|
469
|
-
s.stop(origins.length === 0 ? "No local OIDC servers found." : `Found ${origins.length} local OIDC server${origins.length === 1 ? "" : "s"}.`);
|
|
470
|
-
return origins;
|
|
471
|
-
} catch (error) {
|
|
472
|
-
s.stop("Discovery skipped.");
|
|
473
|
-
throw error;
|
|
474
|
-
}
|
|
554
|
+
s.start("Checking for a local Zitadel server");
|
|
555
|
+
const detected = await detectHealthyLocalServer(cwd);
|
|
556
|
+
s.stop(detected ? `Found local Zitadel server at ${detected}.` : "No local Zitadel server detected.");
|
|
557
|
+
return detected;
|
|
475
558
|
}
|
|
476
559
|
//#endregion
|
|
477
560
|
//#region src/commands/setup/prompts/sign-in-preset.ts
|
|
@@ -504,6 +587,48 @@ var SignInPresetPrompt = class {
|
|
|
504
587
|
}
|
|
505
588
|
};
|
|
506
589
|
//#endregion
|
|
590
|
+
//#region src/commands/setup/prompts/use-case.ts
|
|
591
|
+
/**
|
|
592
|
+
* "Who will sign in to your app?" — picks the use case, which owns the
|
|
593
|
+
* schema field set the scaffold collects (#448). Orthogonal to the sign-in
|
|
594
|
+
* preset, so this asks first: what a user *is* before *how* they sign in.
|
|
595
|
+
* `--use-case` is authoritative; non-interactive runs keep the minimal
|
|
596
|
+
* default seeded by the command.
|
|
597
|
+
*/
|
|
598
|
+
var UseCasePrompt = class {
|
|
599
|
+
async ask(answers, ctx) {
|
|
600
|
+
if (ctx.useCaseFromFlag) return answers;
|
|
601
|
+
const options = {
|
|
602
|
+
minimal: {
|
|
603
|
+
label: "Just me or a small group",
|
|
604
|
+
hint: "collects email"
|
|
605
|
+
},
|
|
606
|
+
consumer: {
|
|
607
|
+
label: "Consumers",
|
|
608
|
+
hint: "collects email, given & family name"
|
|
609
|
+
},
|
|
610
|
+
business: {
|
|
611
|
+
label: "Business",
|
|
612
|
+
hint: "collects email, given & family name, company"
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
const value = await select({
|
|
616
|
+
message: "Who will sign in to your app? Collected fields stay editable after setup.",
|
|
617
|
+
initialValue: answers.useCase,
|
|
618
|
+
options: SETUP_USE_CASES.map((useCase) => ({
|
|
619
|
+
value: useCase,
|
|
620
|
+
label: options[useCase].label,
|
|
621
|
+
hint: options[useCase].hint
|
|
622
|
+
}))
|
|
623
|
+
});
|
|
624
|
+
bail(value);
|
|
625
|
+
return {
|
|
626
|
+
...answers,
|
|
627
|
+
useCase: value
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
//#endregion
|
|
507
632
|
//#region src/commands/setup/prompts/pick-framework.ts
|
|
508
633
|
/**
|
|
509
634
|
* "Choose a framework to scaffold" — the only prompt outside the main wizard.
|
|
@@ -543,7 +668,9 @@ const SETUP_PROMPTS = [
|
|
|
543
668
|
new FrameworkConfirmPrompt(),
|
|
544
669
|
new ServerPrompt(),
|
|
545
670
|
new DevPortPrompt(),
|
|
546
|
-
new
|
|
671
|
+
new UseCasePrompt(),
|
|
672
|
+
new SignInPresetPrompt(),
|
|
673
|
+
new DesignPrompt()
|
|
547
674
|
];
|
|
548
675
|
//#endregion
|
|
549
676
|
//#region src/commands/setup/summary.ts
|
|
@@ -576,6 +703,8 @@ const path = (s) => pc.cyan(s);
|
|
|
576
703
|
const url = (s) => pc.cyan(s);
|
|
577
704
|
/** Yellow, for opaque ids the user shouldn't try to read. */
|
|
578
705
|
const id = (s) => pc.yellow(s);
|
|
706
|
+
/** Dim, for separators and secondary detail. */
|
|
707
|
+
const dim = (s) => pc.dim(s);
|
|
579
708
|
/**
|
|
580
709
|
* Reads the project root to identify the framework version, TS presence,
|
|
581
710
|
* and which package manager the user runs. Returns safe defaults (npm
|
|
@@ -645,6 +774,16 @@ function fileNameOf(p) {
|
|
|
645
774
|
* load.
|
|
646
775
|
*/
|
|
647
776
|
const FRAMEWORK_OPTIONS = createOrca().availableFrameworks().map((framework) => framework.id);
|
|
777
|
+
/**
|
|
778
|
+
* `--renderer` offers only ids `getRenderer` will resolve: a
|
|
779
|
+
* declared-but-unpublished renderer (ADR 006) keeps its registry entry to
|
|
780
|
+
* reserve the id, but is surfaced as unavailable in the flag description
|
|
781
|
+
* instead of in `options`, so `--help` never advertises a value that is
|
|
782
|
+
* guaranteed to fail and an explicit pass is rejected at parse time — before
|
|
783
|
+
* any remote project is created.
|
|
784
|
+
*/
|
|
785
|
+
const UNAVAILABLE_RENDERER_IDS = RENDERER_IDS.filter((id) => !AVAILABLE_RENDERER_IDS.includes(id));
|
|
786
|
+
const RENDERER_FLAG_DESCRIPTION = UNAVAILABLE_RENDERER_IDS.length === 0 ? "Renderer (default: react)." : `Renderer (default: react). Not yet available: ${UNAVAILABLE_RENDERER_IDS.join(", ")}.`;
|
|
648
787
|
/** `zitadel setup` — create a project and scaffold local auth.
|
|
649
788
|
*
|
|
650
789
|
* Detects (or, for an empty directory, scaffolds then re-detects) the
|
|
@@ -666,14 +805,22 @@ var Setup = class Setup extends BaseCommand {
|
|
|
666
805
|
options: FRAMEWORK_OPTIONS
|
|
667
806
|
}),
|
|
668
807
|
renderer: Flags.string({
|
|
669
|
-
description:
|
|
670
|
-
options: [...
|
|
808
|
+
description: RENDERER_FLAG_DESCRIPTION,
|
|
809
|
+
options: [...AVAILABLE_RENDERER_IDS]
|
|
671
810
|
}),
|
|
672
811
|
"dev-port": Flags.integer({ description: "Dev-server port; also the issuer origin registered with Zitadel. Defaults to the detected port. Use distinct ports to run several scaffolded apps side by side." }),
|
|
673
812
|
"skip-install": Flags.boolean({ description: "Do not install dependencies after setup updates package.json." }),
|
|
674
813
|
preset: Flags.string({
|
|
675
814
|
description: "Sign-in preset for the scaffolded schema and login flow (default: password-first).",
|
|
676
815
|
options: [...SETUP_PRESETS]
|
|
816
|
+
}),
|
|
817
|
+
"use-case": Flags.string({
|
|
818
|
+
description: "Use case for the scaffolded schema fields: who signs in to the app (default: minimal).",
|
|
819
|
+
options: [...SETUP_USE_CASES]
|
|
820
|
+
}),
|
|
821
|
+
design: Flags.string({
|
|
822
|
+
description: "Login design to eject into .zitadel/branding/ and publish as branding revision 1. Skips the wizard's design question. When omitted in non-interactive runs, the login uses the built-in template; run the `branding eject` command later to customize.",
|
|
823
|
+
options: [...BRANDING_DESIGNS]
|
|
677
824
|
})
|
|
678
825
|
};
|
|
679
826
|
async run() {
|
|
@@ -713,6 +860,8 @@ var Setup = class Setup extends BaseCommand {
|
|
|
713
860
|
skip_install: Boolean(flags["skip-install"]),
|
|
714
861
|
dev_port_explicit: flags["dev-port"] !== void 0,
|
|
715
862
|
preset: flags.preset ?? DEFAULT_SETUP_PRESET,
|
|
863
|
+
use_case: flags["use-case"] ?? DEFAULT_SETUP_USE_CASE,
|
|
864
|
+
design: flags.design ?? "built-in",
|
|
716
865
|
step: "framework_resolved"
|
|
717
866
|
});
|
|
718
867
|
if (flags["dev-port"] !== void 0) {
|
|
@@ -727,20 +876,29 @@ var Setup = class Setup extends BaseCommand {
|
|
|
727
876
|
let answers = {
|
|
728
877
|
server: this.meta.source,
|
|
729
878
|
devPort: framework.devPort,
|
|
730
|
-
preset: flags.preset ?? DEFAULT_SETUP_PRESET
|
|
879
|
+
preset: flags.preset ?? DEFAULT_SETUP_PRESET,
|
|
880
|
+
useCase: flags["use-case"] ?? DEFAULT_SETUP_USE_CASE,
|
|
881
|
+
design: flags.design
|
|
731
882
|
};
|
|
732
883
|
if (!nonInteractive && !dryRun) {
|
|
733
884
|
intro("Zitadel setup");
|
|
734
885
|
const promptCtx = {
|
|
735
886
|
framework,
|
|
887
|
+
cwd,
|
|
736
888
|
serverFlag: this.meta.serverFlag,
|
|
737
889
|
devPortFromFlag: flags["dev-port"] !== void 0,
|
|
738
|
-
presetFromFlag: flags.preset !== void 0
|
|
890
|
+
presetFromFlag: flags.preset !== void 0,
|
|
891
|
+
useCaseFromFlag: flags["use-case"] !== void 0,
|
|
892
|
+
designFromFlag: flags.design !== void 0
|
|
739
893
|
};
|
|
740
894
|
for (const prompt of SETUP_PROMPTS) answers = await prompt.ask(answers, promptCtx);
|
|
741
895
|
outro("Configuration captured");
|
|
742
896
|
}
|
|
743
|
-
this.recordTelemetry({
|
|
897
|
+
this.recordTelemetry({
|
|
898
|
+
preset: answers.preset,
|
|
899
|
+
use_case: answers.useCase,
|
|
900
|
+
design: answers.design ?? "built-in"
|
|
901
|
+
});
|
|
744
902
|
const issuer = issuerFromPort(answers.devPort);
|
|
745
903
|
framework = {
|
|
746
904
|
...framework,
|
|
@@ -754,10 +912,13 @@ var Setup = class Setup extends BaseCommand {
|
|
|
754
912
|
...retryOptionsFromFlags(flags),
|
|
755
913
|
framework: framework.id,
|
|
756
914
|
preset: answers.preset,
|
|
915
|
+
useCase: answers.useCase,
|
|
916
|
+
design: answers.design,
|
|
757
917
|
devPort: answers.devPort
|
|
758
918
|
});
|
|
759
919
|
consola$1.success(`Created project ${project.id}`);
|
|
760
920
|
this.recordTelemetry({ step: "project_created" });
|
|
921
|
+
const posture = derivePosture(framework.id, scaffoldedFramework);
|
|
761
922
|
const ctx = {
|
|
762
923
|
framework,
|
|
763
924
|
rendererId: flags.renderer ?? "react",
|
|
@@ -766,7 +927,9 @@ var Setup = class Setup extends BaseCommand {
|
|
|
766
927
|
server: answers.server,
|
|
767
928
|
cliVersion: this.meta.cliVersion,
|
|
768
929
|
scaffoldedFramework,
|
|
769
|
-
|
|
930
|
+
posture,
|
|
931
|
+
preset: answers.preset,
|
|
932
|
+
useCase: answers.useCase
|
|
770
933
|
};
|
|
771
934
|
consola$1.start(`Patching project files${dryRun ? " (dry run)" : ""}`);
|
|
772
935
|
const result = await orca.patcherFor(framework.id).patch(ctx, {
|
|
@@ -785,11 +948,13 @@ var Setup = class Setup extends BaseCommand {
|
|
|
785
948
|
cwd,
|
|
786
949
|
client: createZitadelClient({
|
|
787
950
|
baseUrl: answers.server,
|
|
788
|
-
token: project.
|
|
951
|
+
token: project.project_secret
|
|
789
952
|
}),
|
|
790
953
|
projectId: project.id,
|
|
791
954
|
force,
|
|
792
|
-
preset: answers.preset
|
|
955
|
+
preset: answers.preset,
|
|
956
|
+
useCase: answers.useCase,
|
|
957
|
+
design: answers.design
|
|
793
958
|
});
|
|
794
959
|
} catch (error) {
|
|
795
960
|
await rm(join(cwd, "zitadel.json"), { force: true });
|
|
@@ -811,6 +976,21 @@ var Setup = class Setup extends BaseCommand {
|
|
|
811
976
|
step: "files_patched",
|
|
812
977
|
files_written_count: allFilesWritten.length
|
|
813
978
|
});
|
|
979
|
+
if (!dryRun) try {
|
|
980
|
+
await writeScaffoldManifest({
|
|
981
|
+
cwd,
|
|
982
|
+
actions: orca.patcherFor(framework.id).artifacts({
|
|
983
|
+
framework,
|
|
984
|
+
rendererId: ctx.rendererId
|
|
985
|
+
}),
|
|
986
|
+
written: [...result.filesWritten, ...result.filesSkipped],
|
|
987
|
+
scaffoldedFramework,
|
|
988
|
+
devPort: answers.devPort,
|
|
989
|
+
posture
|
|
990
|
+
});
|
|
991
|
+
} catch (error) {
|
|
992
|
+
consola$1.debug("Failed to record the scaffold manifest", error);
|
|
993
|
+
}
|
|
814
994
|
const installOutcome = await installDependenciesForSetup({
|
|
815
995
|
cliVersion: this.meta.cliVersion,
|
|
816
996
|
cwd,
|
|
@@ -827,6 +1007,16 @@ var Setup = class Setup extends BaseCommand {
|
|
|
827
1007
|
package_manager: installOutcome.install.package_manager
|
|
828
1008
|
});
|
|
829
1009
|
const writtenRel = allFilesWritten.map((file) => relativeDisplay(cwd, file));
|
|
1010
|
+
const claimNudge = claimState({
|
|
1011
|
+
secret: {},
|
|
1012
|
+
server: answers.server
|
|
1013
|
+
}).kind === "detached" ? {
|
|
1014
|
+
actions: [claimAction(this.meta.cliVersion)],
|
|
1015
|
+
commands: [claimCommand(this.meta.cliVersion)]
|
|
1016
|
+
} : {
|
|
1017
|
+
actions: [],
|
|
1018
|
+
commands: []
|
|
1019
|
+
};
|
|
830
1020
|
if (!this.jsonEnabled()) {
|
|
831
1021
|
const sections = buildSummary({
|
|
832
1022
|
projectFacts: await detectProjectFacts(cwd, framework.id),
|
|
@@ -834,14 +1024,15 @@ var Setup = class Setup extends BaseCommand {
|
|
|
834
1024
|
project,
|
|
835
1025
|
server: answers.server,
|
|
836
1026
|
issuer,
|
|
837
|
-
scaffoldedFramework
|
|
1027
|
+
scaffoldedFramework,
|
|
1028
|
+
design: answers.design
|
|
838
1029
|
});
|
|
839
1030
|
consola$1.box({
|
|
840
1031
|
title: "Zitadel is ready",
|
|
841
1032
|
message: [
|
|
842
1033
|
renderSummary(sections),
|
|
843
1034
|
"",
|
|
844
|
-
installOutcome.boxActions.join("\n")
|
|
1035
|
+
[...installOutcome.boxActions, ...claimNudge.actions].join("\n")
|
|
845
1036
|
].join("\n"),
|
|
846
1037
|
style: {
|
|
847
1038
|
padding: 1,
|
|
@@ -862,10 +1053,20 @@ var Setup = class Setup extends BaseCommand {
|
|
|
862
1053
|
framework: framework.id,
|
|
863
1054
|
server: answers.server,
|
|
864
1055
|
files_written: allFilesWritten.map((file) => relativeDisplay(cwd, file)),
|
|
1056
|
+
files: result.files.map((file) => ({
|
|
1057
|
+
path: relativeDisplay(cwd, file.path),
|
|
1058
|
+
kind: file.kind,
|
|
1059
|
+
action: file.action
|
|
1060
|
+
})),
|
|
865
1061
|
files_skipped: result.filesSkipped.map((file) => relativeDisplay(cwd, file)),
|
|
866
1062
|
install: installOutcome.install,
|
|
867
|
-
|
|
868
|
-
|
|
1063
|
+
design: answers.design ?? null,
|
|
1064
|
+
next_actions: [
|
|
1065
|
+
...installOutcome.nextActions,
|
|
1066
|
+
brandingGuidanceAction(answers.design, this.meta.cliVersion),
|
|
1067
|
+
...claimNudge.actions
|
|
1068
|
+
],
|
|
1069
|
+
next_commands: [...installOutcome.nextCommands, ...claimNudge.commands]
|
|
869
1070
|
}
|
|
870
1071
|
});
|
|
871
1072
|
}
|
|
@@ -894,10 +1095,10 @@ async function resolveScaffoldFramework(framework, nonInteractive, orca) {
|
|
|
894
1095
|
function dryRunProject(issuer) {
|
|
895
1096
|
return {
|
|
896
1097
|
id: "dry-run-0000",
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1098
|
+
project_secret: "sk_proj_dry_run_full",
|
|
1099
|
+
preview_secret: "sk_proj_dry_run_preview",
|
|
1100
|
+
preview_origins: [issuer],
|
|
1101
|
+
created_at: "2026-04-21T14:03:11.000Z"
|
|
901
1102
|
};
|
|
902
1103
|
}
|
|
903
1104
|
/**
|
|
@@ -909,6 +1110,8 @@ function setupRetryFlags(opts) {
|
|
|
909
1110
|
const parts = [];
|
|
910
1111
|
if (opts.framework) parts.push(`--framework ${opts.framework}`);
|
|
911
1112
|
if (opts.preset && opts.preset !== DEFAULT_SETUP_PRESET) parts.push(`--preset ${opts.preset}`);
|
|
1113
|
+
if (opts.useCase && opts.useCase !== DEFAULT_SETUP_USE_CASE) parts.push(`--use-case ${opts.useCase}`);
|
|
1114
|
+
if (opts.design) parts.push(`--design ${opts.design}`);
|
|
912
1115
|
if (opts.renderer && opts.renderer !== "react") parts.push(`--renderer ${opts.renderer}`);
|
|
913
1116
|
if (opts.devPort !== void 0) parts.push(`--dev-port ${opts.devPort}`);
|
|
914
1117
|
if (opts.nonInteractive) parts.push("--non-interactive");
|
|
@@ -924,6 +1127,8 @@ function retryOptionsFromFlags(flags) {
|
|
|
924
1127
|
return {
|
|
925
1128
|
framework: flags.framework,
|
|
926
1129
|
preset: flags.preset,
|
|
1130
|
+
useCase: flags["use-case"],
|
|
1131
|
+
design: flags.design,
|
|
927
1132
|
renderer: flags.renderer,
|
|
928
1133
|
devPort: flags["dev-port"],
|
|
929
1134
|
nonInteractive: Boolean(flags["non-interactive"])
|
|
@@ -933,8 +1138,8 @@ async function createProjectWithLocalHint(client, server, cliVersion, projectNam
|
|
|
933
1138
|
try {
|
|
934
1139
|
const payload = {
|
|
935
1140
|
name: projectName,
|
|
936
|
-
|
|
937
|
-
|
|
1141
|
+
preview_origins: [issuer],
|
|
1142
|
+
seed_defaults: false
|
|
938
1143
|
};
|
|
939
1144
|
return await client.createProject(payload);
|
|
940
1145
|
} catch (error) {
|
|
@@ -988,15 +1193,13 @@ function pickWrittenFile(written, suffix) {
|
|
|
988
1193
|
}
|
|
989
1194
|
/**
|
|
990
1195
|
* Translates a patcher-written path into a single sentence the user can
|
|
991
|
-
* read at narration speed.
|
|
992
|
-
*
|
|
993
|
-
*
|
|
994
|
-
*
|
|
995
|
-
*
|
|
996
|
-
* something happened.
|
|
1196
|
+
* read at narration speed. The patch result's `filesWritten` carries
|
|
1197
|
+
* deduplicated file paths only (directories stay in the typed `files`
|
|
1198
|
+
* rows), so no artefact filtering is needed here. The verb tense flips
|
|
1199
|
+
* for `--dry-run` so the user sees a preview ("Would write ...") instead
|
|
1200
|
+
* of a claim that something happened.
|
|
997
1201
|
*/
|
|
998
1202
|
function describeWrittenFile(relPath, dryRun) {
|
|
999
|
-
if (relPath === ".zitadel" || relPath === ".zitadel/flows" || relPath === ".zitadel/schemas" || relPath === ".zitadel/meta") return null;
|
|
1000
1203
|
const verb = dryRun ? "Would write" : "Wrote";
|
|
1001
1204
|
const sentence = SENTENCE_BY_PATH[relPath];
|
|
1002
1205
|
if (sentence) return `${verb} ${sentence.subject} (${path(relPath)})`;
|
|
@@ -1035,7 +1238,7 @@ const SENTENCE_BY_PATH = {
|
|
|
1035
1238
|
};
|
|
1036
1239
|
/** Builds the section list driving {@link renderSummary} for the setup command. */
|
|
1037
1240
|
function buildSummary(opts) {
|
|
1038
|
-
const { projectFacts, writtenRel, project, server, issuer, scaffoldedFramework } = opts;
|
|
1241
|
+
const { projectFacts, writtenRel, project, server, issuer, scaffoldedFramework, design } = opts;
|
|
1039
1242
|
const sdkPackage = "@zitadel/sdk-next";
|
|
1040
1243
|
const packageJsonHit = pickWrittenFile(writtenRel, "package.json");
|
|
1041
1244
|
const detected = [{
|
|
@@ -1079,6 +1282,14 @@ function buildSummary(opts) {
|
|
|
1079
1282
|
{
|
|
1080
1283
|
label: "App will run",
|
|
1081
1284
|
value: url(issuer)
|
|
1285
|
+
},
|
|
1286
|
+
design ? {
|
|
1287
|
+
label: "Login design",
|
|
1288
|
+
value: design,
|
|
1289
|
+
secondary: path(".zitadel/branding/")
|
|
1290
|
+
} : {
|
|
1291
|
+
label: "Login design",
|
|
1292
|
+
value: dim("built-in template")
|
|
1082
1293
|
}
|
|
1083
1294
|
];
|
|
1084
1295
|
return [
|