@zitadel/cli 0.1.0-alpha.9 → 1.0.0-alpha.21
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 +250 -62
- package/SKILLS.md +221 -37
- package/dist/branding-cygSBPkV.mjs +79 -0
- package/dist/branding-cygSBPkV.mjs.map +1 -0
- package/dist/claim-state-DlfD6Res.mjs +66 -0
- package/dist/claim-state-DlfD6Res.mjs.map +1 -0
- package/dist/commands/apply.mjs +27 -8
- package/dist/commands/apply.mjs.map +1 -1
- package/dist/commands/branding/eject.mjs +104 -0
- package/dist/commands/branding/eject.mjs.map +1 -0
- package/dist/commands/claim.mjs +415 -0
- package/dist/commands/claim.mjs.map +1 -0
- package/dist/commands/doctor.mjs +594 -41
- package/dist/commands/doctor.mjs.map +1 -1
- package/dist/commands/eject.mjs +18 -6
- package/dist/commands/eject.mjs.map +1 -1
- package/dist/commands/logs.mjs +2 -2
- package/dist/commands/plan.mjs +19 -6
- package/dist/commands/plan.mjs.map +1 -1
- package/dist/commands/reset.mjs +2 -2
- package/dist/commands/schemas/list.mjs +146 -0
- package/dist/commands/schemas/list.mjs.map +1 -0
- package/dist/commands/setup.mjs +629 -214
- package/dist/commands/setup.mjs.map +1 -1
- package/dist/commands/start.mjs +4 -4
- package/dist/commands/start.mjs.map +1 -1
- package/dist/commands/status.mjs +52 -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/designs-Ckz18Dpo.mjs +38 -0
- package/dist/designs-Ckz18Dpo.mjs.map +1 -0
- package/dist/{docker-CnGQK3ZK.mjs → docker-Ev1Ggt9l.mjs} +10 -6
- package/dist/docker-Ev1Ggt9l.mjs.map +1 -0
- package/dist/environment-rjRVkJjW.mjs +17 -0
- package/dist/environment-rjRVkJjW.mjs.map +1 -0
- package/dist/journey-guidance-BGrOX_gT.mjs +40 -0
- package/dist/journey-guidance-BGrOX_gT.mjs.map +1 -0
- package/dist/oclif-CanO3zdt.mjs +1769 -0
- package/dist/oclif-CanO3zdt.mjs.map +1 -0
- package/dist/{orca-U142Wrau.mjs → orca-66vAxeIe.mjs} +1666 -305
- package/dist/orca-66vAxeIe.mjs.map +1 -0
- package/dist/package-manager-DFdLcDx1.mjs +194 -0
- package/dist/package-manager-DFdLcDx1.mjs.map +1 -0
- package/dist/{ports-B09RjuHx.mjs → ports-BM20XIZb.mjs} +25 -20
- package/dist/ports-BM20XIZb.mjs.map +1 -0
- package/dist/{processes-Cw8TO1SY.mjs → processes-DKHPkU8O.mjs} +1 -1
- package/dist/{processes-Cw8TO1SY.mjs.map → processes-DKHPkU8O.mjs.map} +1 -1
- package/dist/{project-Cd0L3PtM.mjs → project-CHhot85s.mjs} +57 -4
- package/dist/project-CHhot85s.mjs.map +1 -0
- package/dist/sync-BBlACKzT.mjs +1633 -0
- package/dist/sync-BBlACKzT.mjs.map +1 -0
- package/dist/user-schema-DTuOsdKE.mjs +91 -0
- package/dist/user-schema-DTuOsdKE.mjs.map +1 -0
- package/oclif.manifest.json +406 -5
- package/package.json +10 -5
- package/dist/docker-CnGQK3ZK.mjs.map +0 -1
- package/dist/docker-guidance-ypN3IM3o.mjs +0 -21
- package/dist/docker-guidance-ypN3IM3o.mjs.map +0 -1
- package/dist/oclif-B7lBzh3R.mjs +0 -808
- package/dist/oclif-B7lBzh3R.mjs.map +0 -1
- package/dist/orca-U142Wrau.mjs.map +0 -1
- package/dist/ports-B09RjuHx.mjs.map +0 -1
- package/dist/project-Cd0L3PtM.mjs.map +0 -1
- package/dist/sync-BojoQm2P.mjs +0 -733
- package/dist/sync-BojoQm2P.mjs.map +0 -1
package/dist/commands/setup.mjs
CHANGED
|
@@ -1,96 +1,181 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { n as
|
|
4
|
-
import { t as
|
|
1
|
+
import { n as brandingDesignLabel, t as BRANDING_DESIGN_INFO } from "../designs-Ckz18Dpo.mjs";
|
|
2
|
+
import { D as toZitadelError, E as ZitadelError, T as stableStringify, j as publicCliCommand, k as normalizePublicCliProse, n as DEFAULT_SERVER, t as BaseCommand, u as detectHealthyLocalServer } from "../oclif-CanO3zdt.mjs";
|
|
3
|
+
import { n as claimCommand, r as claimState, t as claimAction } from "../claim-state-DlfD6Res.mjs";
|
|
4
|
+
import { n as customizeAndPublishActions, r as verifyLoginAction, t as brandingGuidanceAction } from "../journey-guidance-BGrOX_gT.mjs";
|
|
5
|
+
import { i as RENDERER_IDS, n as inspectScaffoldTarget, r as AVAILABLE_RENDERER_IDS, s as issuerFromPort, t as createOrca } from "../orca-66vAxeIe.mjs";
|
|
6
|
+
import { n as hasZitadelSecret, t as hasZitadelConfig } from "../project-CHhot85s.mjs";
|
|
7
|
+
import { a as updateState, t as SCHEMAS_DIR } from "../user-schema-DTuOsdKE.mjs";
|
|
8
|
+
import { a as runPackageCommand, c as writeScaffoldManifest, i as installCommandFor, n as detectPackageManager, r as devCommandFor } from "../package-manager-DFdLcDx1.mjs";
|
|
9
|
+
import { i as toBrandingWireBody, t as BRANDING_DIR } from "../branding-cygSBPkV.mjs";
|
|
10
|
+
import { c as writeBackResource, o as hashForState, u as FLOWS_DIR } from "../sync-BBlACKzT.mjs";
|
|
11
|
+
import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
12
|
+
import { basename, dirname, join } from "node:path";
|
|
5
13
|
import { cancel, confirm, intro, isCancel, outro, select, spinner, text } from "@clack/prompts";
|
|
6
14
|
import { Flags } from "@oclif/core";
|
|
7
15
|
import { createZitadelClient } from "@zitadel/api/client";
|
|
16
|
+
import { BRANDING_DESIGNS, DEFAULT_BRANDING_CONFIG_PATH, DEFAULT_BRANDING_TEMPLATE_PATH, DEFAULT_FLOW_CONFIG_PATH, DEFAULT_FLOW_SCHEMA_URI, DEFAULT_SCHEMA_CONFIG_PATH, DEFAULT_SETUP_PRESET, DEFAULT_SETUP_USE_CASE, SETUP_PRESETS, SETUP_USE_CASES, brandingReadmeContent, flowsReadmeContent, getDefaultBrandingConfig, getDefaultHumanUserSchema, getDefaultLoginFlow, schemasReadmeContent } from "@zitadel/config/defaults";
|
|
8
17
|
import { consola as consola$1 } from "consola";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { spawn } from "node:child_process";
|
|
18
|
+
import { BRANDING_FILE_SCHEMA_REF } from "@zitadel/config/meta-schemas";
|
|
19
|
+
import { normalizeFlowBody, normalizeSchemaBody } from "@zitadel/config/normalize";
|
|
12
20
|
import pc from "picocolors";
|
|
13
|
-
//#region src/lib/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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";
|
|
34
47
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const error = /* @__PURE__ */ new Error(`${packageCommand.display} failed with ${detail}`);
|
|
58
|
-
Object.assign(error, {
|
|
59
|
-
code: code ?? 1,
|
|
60
|
-
signal
|
|
61
|
-
});
|
|
62
|
-
reject(error);
|
|
63
|
-
});
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/lib/setup-resources.ts
|
|
50
|
+
/**
|
|
51
|
+
* Scaffolds the versioned local default resources for a new project, uploads
|
|
52
|
+
* them through the schema/flow APIs, and seeds `.zitadel/state.json` with the
|
|
53
|
+
* IDs, hashes, and flow metadata the sync engine expects. Setup calls this only
|
|
54
|
+
* after the framework patcher has created `.zitadel/{flows,schemas}` and the
|
|
55
|
+
* initial state file.
|
|
56
|
+
*
|
|
57
|
+
* The schema is uploaded without an `$id`: the server assigns an opaque id on
|
|
58
|
+
* `POST /schemas`, and the flow file can only be rendered after that id comes
|
|
59
|
+
* back because `flow_definition.user_schema` must reference it.
|
|
60
|
+
*/
|
|
61
|
+
async function materializeSetupResources(opts) {
|
|
62
|
+
await mkdir(join(opts.cwd, FLOWS_DIR), { recursive: true });
|
|
63
|
+
await mkdir(join(opts.cwd, SCHEMAS_DIR), { recursive: true });
|
|
64
|
+
const filesWritten = [];
|
|
65
|
+
const preset = opts.preset ?? DEFAULT_SETUP_PRESET;
|
|
66
|
+
const useCase = opts.useCase ?? DEFAULT_SETUP_USE_CASE;
|
|
67
|
+
const { $id: _templateId, ...schemaBody } = getDefaultHumanUserSchema({
|
|
68
|
+
preset,
|
|
69
|
+
useCase
|
|
64
70
|
});
|
|
71
|
+
if (await writeResourceFile(opts.cwd, DEFAULT_SCHEMA_CONFIG_PATH, schemaBody, opts.force)) filesWritten.push(join(opts.cwd, DEFAULT_SCHEMA_CONFIG_PATH));
|
|
72
|
+
const schemaId = requiredString((await opts.client.createSchema(schemaBody, { project_id: opts.projectId })).id, "created schema id");
|
|
73
|
+
let schemaHash = hashForState({ normalize: normalizeSchemaBody }, schemaBody);
|
|
74
|
+
try {
|
|
75
|
+
const canonical = (await opts.client.getSchemaById(encodeURIComponent(schemaId))).schema;
|
|
76
|
+
schemaHash = (await writeBackResource(opts.cwd, DEFAULT_SCHEMA_CONFIG_PATH, { normalize: normalizeSchemaBody }, canonical)).hash;
|
|
77
|
+
} catch (err) {
|
|
78
|
+
consola$1.debug(`fetch created schema ${schemaId} during setup failed:`, err);
|
|
79
|
+
}
|
|
80
|
+
await updateState(opts.cwd, DEFAULT_SCHEMA_CONFIG_PATH, {
|
|
81
|
+
id: schemaId,
|
|
82
|
+
hash: schemaHash
|
|
83
|
+
});
|
|
84
|
+
const flowBody = getDefaultLoginFlow({
|
|
85
|
+
userSchemaUrl: schemaId,
|
|
86
|
+
preset,
|
|
87
|
+
useCase
|
|
88
|
+
});
|
|
89
|
+
if (await writeResourceFile(opts.cwd, DEFAULT_FLOW_CONFIG_PATH, flowBody, opts.force)) filesWritten.push(join(opts.cwd, DEFAULT_FLOW_CONFIG_PATH));
|
|
90
|
+
const flow = await opts.client.createFlowDefinition({
|
|
91
|
+
project_id: opts.projectId,
|
|
92
|
+
schema_uri: DEFAULT_FLOW_SCHEMA_URI,
|
|
93
|
+
flow_definition: flowBody
|
|
94
|
+
});
|
|
95
|
+
let flowHash = hashForState({ normalize: normalizeFlowBody }, flowBody);
|
|
96
|
+
if (flow.flow_definition) flowHash = (await writeBackResource(opts.cwd, DEFAULT_FLOW_CONFIG_PATH, {
|
|
97
|
+
normalize: normalizeFlowBody,
|
|
98
|
+
normalizeWrite: normalizeFlowBody
|
|
99
|
+
}, flow.flow_definition)).hash;
|
|
100
|
+
await updateState(opts.cwd, DEFAULT_FLOW_CONFIG_PATH, {
|
|
101
|
+
id: requiredString(flow.id, "created flow definition id"),
|
|
102
|
+
hash: flowHash,
|
|
103
|
+
name: flowBody.name,
|
|
104
|
+
status: flowBody.status
|
|
105
|
+
});
|
|
106
|
+
if (opts.design) {
|
|
107
|
+
await mkdir(join(opts.cwd, BRANDING_DIR), { recursive: true });
|
|
108
|
+
const { branding, template } = getDefaultBrandingConfig(opts.design);
|
|
109
|
+
const descriptor = {
|
|
110
|
+
$schema: BRANDING_FILE_SCHEMA_REF,
|
|
111
|
+
...branding
|
|
112
|
+
};
|
|
113
|
+
if (await writeResourceFile(opts.cwd, DEFAULT_BRANDING_CONFIG_PATH, descriptor, opts.force)) filesWritten.push(join(opts.cwd, DEFAULT_BRANDING_CONFIG_PATH));
|
|
114
|
+
if (await writeRawFile(opts.cwd, DEFAULT_BRANDING_TEMPLATE_PATH, template, opts.force)) filesWritten.push(join(opts.cwd, DEFAULT_BRANDING_TEMPLATE_PATH));
|
|
115
|
+
const brandingNormalize = (data) => toBrandingWireBody(opts.cwd, data);
|
|
116
|
+
const created = await opts.client.createBranding(brandingNormalize(descriptor), { project_id: opts.projectId });
|
|
117
|
+
await updateState(opts.cwd, DEFAULT_BRANDING_CONFIG_PATH, {
|
|
118
|
+
id: requiredString(created.id, "created branding revision id"),
|
|
119
|
+
hash: hashForState({ normalize: brandingNormalize }, descriptor)
|
|
120
|
+
});
|
|
121
|
+
const brandingReadme = join(BRANDING_DIR, "README.md");
|
|
122
|
+
if (await writeReadmeFile(opts.cwd, brandingReadme, normalizePublicCliProse(brandingReadmeContent(), opts.cliVersion))) filesWritten.push(join(opts.cwd, brandingReadme));
|
|
123
|
+
}
|
|
124
|
+
const schemasReadme = join(SCHEMAS_DIR, "README.md");
|
|
125
|
+
const flowsReadme = join(FLOWS_DIR, "README.md");
|
|
126
|
+
if (await writeReadmeFile(opts.cwd, schemasReadme, normalizePublicCliProse(schemasReadmeContent(), opts.cliVersion))) filesWritten.push(join(opts.cwd, schemasReadme));
|
|
127
|
+
if (await writeReadmeFile(opts.cwd, flowsReadme, normalizePublicCliProse(flowsReadmeContent(), opts.cliVersion))) filesWritten.push(join(opts.cwd, flowsReadme));
|
|
128
|
+
return { filesWritten };
|
|
65
129
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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 });
|
|
74
138
|
try {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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;
|
|
80
144
|
}
|
|
81
145
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Write a README file, but never overwrite an existing one. A developer who
|
|
148
|
+
* has edited the README should keep their edits when `setup --force` is
|
|
149
|
+
* re-run.
|
|
150
|
+
*/
|
|
151
|
+
async function writeReadmeFile(cwd, relPath, content) {
|
|
152
|
+
const dest = join(cwd, relPath);
|
|
153
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
154
|
+
try {
|
|
155
|
+
await writeFile(dest, content, { flag: "wx" });
|
|
156
|
+
return true;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (isErrno(error, "EEXIST")) return false;
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
85
161
|
}
|
|
86
|
-
async function
|
|
162
|
+
async function writeResourceFile(cwd, relPath, body, force) {
|
|
163
|
+
const contents = `${stableStringify(body)}\n`;
|
|
87
164
|
try {
|
|
88
|
-
await
|
|
165
|
+
await writeFile(join(cwd, relPath), contents, force ? void 0 : { flag: "wx" });
|
|
89
166
|
return true;
|
|
90
|
-
} catch {
|
|
91
|
-
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (isErrno(error, "EEXIST")) throw new ZitadelError("E_CONFLICT", `${relPath} already exists`, { hint: "Move the file aside or rerun setup with --force if you want setup to replace it." });
|
|
169
|
+
throw error;
|
|
92
170
|
}
|
|
93
171
|
}
|
|
172
|
+
function requiredString(value, label) {
|
|
173
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
174
|
+
throw new ZitadelError("E_VALIDATION", `Missing ${label} in server response.`);
|
|
175
|
+
}
|
|
176
|
+
function isErrno(error, code) {
|
|
177
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
178
|
+
}
|
|
94
179
|
//#endregion
|
|
95
180
|
//#region src/commands/setup/install.ts
|
|
96
181
|
async function installDependenciesForSetup(input) {
|
|
@@ -105,6 +190,7 @@ async function installDependenciesForSetup(input) {
|
|
|
105
190
|
reason: "no-dependency-changes"
|
|
106
191
|
},
|
|
107
192
|
devCommand: devCommand.display,
|
|
193
|
+
cliVersion: input.cliVersion,
|
|
108
194
|
issuer: input.issuer,
|
|
109
195
|
includeInstallCommand: false
|
|
110
196
|
});
|
|
@@ -116,6 +202,7 @@ async function installDependenciesForSetup(input) {
|
|
|
116
202
|
reason: input.dryRun ? "dry-run" : "skip-install"
|
|
117
203
|
},
|
|
118
204
|
devCommand: devCommand.display,
|
|
205
|
+
cliVersion: input.cliVersion,
|
|
119
206
|
issuer: input.issuer,
|
|
120
207
|
includeInstallCommand: true
|
|
121
208
|
});
|
|
@@ -137,22 +224,30 @@ async function installDependenciesForSetup(input) {
|
|
|
137
224
|
command: installCommand.display
|
|
138
225
|
},
|
|
139
226
|
devCommand: devCommand.display,
|
|
227
|
+
cliVersion: input.cliVersion,
|
|
140
228
|
issuer: input.issuer,
|
|
141
229
|
includeInstallCommand: false
|
|
142
230
|
});
|
|
143
231
|
}
|
|
144
232
|
function outcome(input) {
|
|
145
|
-
const
|
|
146
|
-
const
|
|
233
|
+
const planCommand = publicCliCommand("plan", input.cliVersion);
|
|
234
|
+
const statusCommand = publicCliCommand("status", input.cliVersion);
|
|
235
|
+
const verifyActions = [
|
|
236
|
+
...input.includeInstallCommand ? [`Install dependencies: ${input.install.command}`] : [],
|
|
237
|
+
`Start your project: ${input.devCommand} (then open ${input.issuer}/login)`,
|
|
238
|
+
verifyLoginAction()
|
|
239
|
+
];
|
|
240
|
+
const breadcrumb = `Once login works: ${statusCommand} shows your next steps; customizing is covered in your README's Zitadel section.`;
|
|
147
241
|
return {
|
|
148
242
|
install: input.install,
|
|
149
243
|
devCommand: input.devCommand,
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
244
|
+
boxActions: [...verifyActions, breadcrumb],
|
|
245
|
+
nextActions: [...verifyActions, ...customizeAndPublishActions(input.cliVersion)],
|
|
246
|
+
nextCommands: input.includeInstallCommand ? [
|
|
247
|
+
input.install.command,
|
|
248
|
+
input.devCommand,
|
|
249
|
+
planCommand
|
|
250
|
+
] : [input.devCommand, planCommand]
|
|
156
251
|
};
|
|
157
252
|
}
|
|
158
253
|
function installFailed(error, installCommand, devCommand, cwd) {
|
|
@@ -188,6 +283,54 @@ function bail(value) {
|
|
|
188
283
|
}
|
|
189
284
|
}
|
|
190
285
|
//#endregion
|
|
286
|
+
//#region src/commands/setup/prompts/design.ts
|
|
287
|
+
/**
|
|
288
|
+
* Sentinel select value for "keep the built-in login" — clack option values
|
|
289
|
+
* must be strings, and `undefined` in {@link SetupAnswers.design} is the
|
|
290
|
+
* real representation of that choice.
|
|
291
|
+
*/
|
|
292
|
+
const BUILT_IN = "built-in";
|
|
293
|
+
/**
|
|
294
|
+
* "How should the login look?" — the last wizard question (#676). Asks after
|
|
295
|
+
* the sign-in preset so the order tells a story: the use case owns what is
|
|
296
|
+
* collected, the preset owns how users authenticate, the design owns how
|
|
297
|
+
* that experience looks.
|
|
298
|
+
*
|
|
299
|
+
* The built-in default is preselected and writes nothing. Picking a starter
|
|
300
|
+
* design forks its Liquid template into `.zitadel/branding/` and publishes
|
|
301
|
+
* it as branding revision 1 — from then on the template is repo-owned and
|
|
302
|
+
* stops tracking built-in improvements, so the option labels make the
|
|
303
|
+
* file-write explicit and keep ownership an opt-in. `--design` is
|
|
304
|
+
* authoritative; non-interactive runs keep the built-in template.
|
|
305
|
+
*/
|
|
306
|
+
var DesignPrompt = class {
|
|
307
|
+
async ask(answers, ctx) {
|
|
308
|
+
if (ctx.designFromFlag) return answers;
|
|
309
|
+
const value = await select({
|
|
310
|
+
message: "How should the login look? A starter design adds its editable template to .zitadel/branding/.",
|
|
311
|
+
initialValue: answers.design ?? BUILT_IN,
|
|
312
|
+
options: [{
|
|
313
|
+
value: BUILT_IN,
|
|
314
|
+
label: "Built-in",
|
|
315
|
+
hint: "no files added; pick a design anytime later with `branding eject`"
|
|
316
|
+
}, ...BRANDING_DESIGNS.map((design) => ({
|
|
317
|
+
value: design,
|
|
318
|
+
label: BRANDING_DESIGN_INFO[design].label,
|
|
319
|
+
hint: BRANDING_DESIGN_INFO[design].hint
|
|
320
|
+
}))]
|
|
321
|
+
});
|
|
322
|
+
bail(value);
|
|
323
|
+
if (value === BUILT_IN) return {
|
|
324
|
+
...answers,
|
|
325
|
+
design: void 0
|
|
326
|
+
};
|
|
327
|
+
return {
|
|
328
|
+
...answers,
|
|
329
|
+
design: value
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
//#endregion
|
|
191
334
|
//#region src/commands/setup/prompts/dev-port.ts
|
|
192
335
|
/**
|
|
193
336
|
* "Dev server port" — defaults to the detected port. The validated answer
|
|
@@ -231,64 +374,27 @@ var FrameworkConfirmPrompt = class {
|
|
|
231
374
|
}
|
|
232
375
|
};
|
|
233
376
|
//#endregion
|
|
234
|
-
//#region src/lib/prober/http.ts
|
|
235
|
-
/**
|
|
236
|
-
* Fetch `url` with a per-call timeout and pass the resulting `Response` to
|
|
237
|
-
* `predicate`. Returns the predicate's value, or `null` on any failure: a
|
|
238
|
-
* network error, an `AbortError` from the timeout, a thrown predicate, or the
|
|
239
|
-
* predicate returning `null`.
|
|
240
|
-
*
|
|
241
|
-
* Never throws. The predicate decides what counts as a match — it gets the
|
|
242
|
-
* raw `Response` and may read `.json()` / `.headers` / `.status` as needed.
|
|
243
|
-
*
|
|
244
|
-
* `timeoutMs` defaults to 500ms. The implementation uses
|
|
245
|
-
* `AbortSignal.timeout`, so the underlying fetch is cancelled when the
|
|
246
|
-
* timeout fires (no orphaned sockets).
|
|
247
|
-
*/
|
|
248
|
-
async function probeUrl(url, predicate, opts) {
|
|
249
|
-
const timeoutMs = opts?.timeoutMs ?? 500;
|
|
250
|
-
try {
|
|
251
|
-
return await predicate(await fetch(url, { signal: AbortSignal.timeout(timeoutMs) }));
|
|
252
|
-
} catch {
|
|
253
|
-
return null;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
/**
|
|
257
|
-
* Probe many URLs in parallel and return the non-null matches, preserving
|
|
258
|
-
* the input order. Each URL is bounded by `timeoutMs` independently — one
|
|
259
|
-
* slow target can't hold up the others.
|
|
260
|
-
*/
|
|
261
|
-
async function probeUrls(urls, predicate, opts) {
|
|
262
|
-
const list = [...urls];
|
|
263
|
-
return (await Promise.all(list.map(async (url) => {
|
|
264
|
-
const value = await probeUrl(url, predicate, opts);
|
|
265
|
-
return value === null ? null : {
|
|
266
|
-
url,
|
|
267
|
-
value
|
|
268
|
-
};
|
|
269
|
-
}))).filter((match) => match !== null);
|
|
270
|
-
}
|
|
271
|
-
//#endregion
|
|
272
377
|
//#region src/commands/setup/prompts/server.ts
|
|
273
378
|
/** Sentinel returned by the choice select when the user picks "Custom URL". */
|
|
274
379
|
const CUSTOM = "__custom__";
|
|
275
|
-
/** Per-probe timeout for the localhost OIDC scan — generous enough for a TLS handshake on a busy laptop. */
|
|
276
|
-
const OIDC_PROBE_TIMEOUT_MS = 300;
|
|
277
380
|
/**
|
|
278
|
-
* "Which server should `zitadel.json` point to?" — Zitadel Cloud,
|
|
279
|
-
*
|
|
381
|
+
* "Which server should `zitadel.json` point to?" — Zitadel Cloud, the managed
|
|
382
|
+
* local server we detected, or a custom URL.
|
|
280
383
|
*
|
|
281
|
-
* Before asking, it
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
384
|
+
* Before asking, it looks for a healthy local Zitadel server the same way the
|
|
385
|
+
* rest of the CLI does (`detectHealthyLocalServer`: the runtime metadata that
|
|
386
|
+
* `zitadel start` wrote to `.zitadel/local/runtime.json`, falling back to a
|
|
387
|
+
* `/healthz` probe on the default localhost port). A detected server becomes
|
|
388
|
+
* an extra option in the choice list *and* the preselected answer — a user
|
|
389
|
+
* who just ran `zitadel start` almost certainly wants it, and `start`'s own
|
|
390
|
+
* next-step hint says `setup --server local`. Picking it writes the URL
|
|
391
|
+
* directly to `answers.server`; picking "Custom URL" still asks for a
|
|
286
392
|
* validated URL exactly as before.
|
|
287
393
|
*/
|
|
288
394
|
var ServerPrompt = class {
|
|
289
395
|
async ask(answers, ctx) {
|
|
290
396
|
if (ctx.serverFlag) return answers;
|
|
291
|
-
const
|
|
397
|
+
const detected = await detectLocalServer(ctx.cwd);
|
|
292
398
|
const choice = await select({
|
|
293
399
|
message: "Which server should zitadel.json point to?",
|
|
294
400
|
options: [
|
|
@@ -297,17 +403,17 @@ var ServerPrompt = class {
|
|
|
297
403
|
label: "Zitadel Cloud (api.zitadel.cloud)",
|
|
298
404
|
hint: "recommended for real projects"
|
|
299
405
|
},
|
|
300
|
-
...
|
|
301
|
-
value:
|
|
302
|
-
label: server
|
|
303
|
-
hint: "detected —
|
|
304
|
-
}
|
|
406
|
+
...detected ? [{
|
|
407
|
+
value: detected,
|
|
408
|
+
label: `Local Zitadel server (${detected})`,
|
|
409
|
+
hint: "detected — started with zitadel start"
|
|
410
|
+
}] : [],
|
|
305
411
|
{
|
|
306
412
|
value: CUSTOM,
|
|
307
413
|
label: "Custom URL (self-hosted)"
|
|
308
414
|
}
|
|
309
415
|
],
|
|
310
|
-
initialValue: answers.server ?? "https://api.zitadel.cloud"
|
|
416
|
+
initialValue: detected ?? answers.server ?? "https://api.zitadel.cloud"
|
|
311
417
|
});
|
|
312
418
|
bail(choice);
|
|
313
419
|
if (choice !== CUSTOM) return {
|
|
@@ -334,38 +440,89 @@ var ServerPrompt = class {
|
|
|
334
440
|
}
|
|
335
441
|
};
|
|
336
442
|
/**
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
* Zitadel-shaped detail here is the OIDC predicate, which is the standard
|
|
341
|
-
* "any OIDC server" contract (no `zitadel`-keyword check), so Keycloak/dex/
|
|
342
|
-
* etc. also surface and the user picks the right one.
|
|
443
|
+
* Wraps {@link detectHealthyLocalServer} in a spinner: the unhealthy paths
|
|
444
|
+
* block on up to two 1.5s `/healthz` probes, long enough that the wizard
|
|
445
|
+
* should say what it is doing.
|
|
343
446
|
*/
|
|
344
|
-
async function
|
|
447
|
+
async function detectLocalServer(cwd) {
|
|
345
448
|
const s = spinner();
|
|
346
|
-
s.start("
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
449
|
+
s.start("Checking for a local Zitadel server");
|
|
450
|
+
const detected = await detectHealthyLocalServer(cwd);
|
|
451
|
+
s.stop(detected ? `Found local Zitadel server at ${detected}.` : "No local Zitadel server detected.");
|
|
452
|
+
return detected;
|
|
453
|
+
}
|
|
454
|
+
//#endregion
|
|
455
|
+
//#region src/commands/setup/prompts/sign-in-preset.ts
|
|
456
|
+
/**
|
|
457
|
+
* "How should users sign in?" — picks the schema+flow preset the scaffold
|
|
458
|
+
* starts from (#448: app-type/preset selection before any `.zitadel/` file
|
|
459
|
+
* is written). `--preset` is authoritative; non-interactive runs keep the
|
|
460
|
+
* password-first default seeded by the command.
|
|
461
|
+
*/
|
|
462
|
+
var SignInPresetPrompt = class {
|
|
463
|
+
async ask(answers, ctx) {
|
|
464
|
+
if (ctx.presetFromFlag) return answers;
|
|
465
|
+
const labels = {
|
|
466
|
+
"password-first": "Password first — email + password, passkey optional during registration",
|
|
467
|
+
"passkey-first": "Passkey first — one-tap passkey up front, email + password fallback"
|
|
468
|
+
};
|
|
469
|
+
const value = await select({
|
|
470
|
+
message: "How should users sign in?",
|
|
471
|
+
initialValue: answers.preset,
|
|
472
|
+
options: SETUP_PRESETS.map((preset) => ({
|
|
473
|
+
value: preset,
|
|
474
|
+
label: labels[preset]
|
|
475
|
+
}))
|
|
476
|
+
});
|
|
477
|
+
bail(value);
|
|
478
|
+
return {
|
|
479
|
+
...answers,
|
|
480
|
+
preset: value
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
//#endregion
|
|
485
|
+
//#region src/commands/setup/prompts/use-case.ts
|
|
486
|
+
/**
|
|
487
|
+
* "Who will sign in to your app?" — picks the use case, which owns the
|
|
488
|
+
* schema field set the scaffold collects (#448). Orthogonal to the sign-in
|
|
489
|
+
* preset, so this asks first: what a user *is* before *how* they sign in.
|
|
490
|
+
* `--use-case` is authoritative; non-interactive runs keep the minimal
|
|
491
|
+
* default seeded by the command.
|
|
492
|
+
*/
|
|
493
|
+
var UseCasePrompt = class {
|
|
494
|
+
async ask(answers, ctx) {
|
|
495
|
+
if (ctx.useCaseFromFlag) return answers;
|
|
496
|
+
const options = {
|
|
497
|
+
minimal: {
|
|
498
|
+
label: "Just me or a small group",
|
|
499
|
+
hint: "collects email"
|
|
500
|
+
},
|
|
501
|
+
consumer: {
|
|
502
|
+
label: "Consumers",
|
|
503
|
+
hint: "collects email, given & family name"
|
|
504
|
+
},
|
|
505
|
+
business: {
|
|
506
|
+
label: "Business",
|
|
507
|
+
hint: "collects email, given & family name, company"
|
|
360
508
|
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
509
|
+
};
|
|
510
|
+
const value = await select({
|
|
511
|
+
message: "Who will sign in to your app? Collected fields stay editable after setup.",
|
|
512
|
+
initialValue: answers.useCase,
|
|
513
|
+
options: SETUP_USE_CASES.map((useCase) => ({
|
|
514
|
+
value: useCase,
|
|
515
|
+
label: options[useCase].label,
|
|
516
|
+
hint: options[useCase].hint
|
|
517
|
+
}))
|
|
518
|
+
});
|
|
519
|
+
bail(value);
|
|
520
|
+
return {
|
|
521
|
+
...answers,
|
|
522
|
+
useCase: value
|
|
523
|
+
};
|
|
367
524
|
}
|
|
368
|
-
}
|
|
525
|
+
};
|
|
369
526
|
//#endregion
|
|
370
527
|
//#region src/commands/setup/prompts/pick-framework.ts
|
|
371
528
|
/**
|
|
@@ -405,11 +562,24 @@ var PickFrameworkPrompt = class {
|
|
|
405
562
|
const SETUP_PROMPTS = [
|
|
406
563
|
new FrameworkConfirmPrompt(),
|
|
407
564
|
new ServerPrompt(),
|
|
408
|
-
new DevPortPrompt()
|
|
565
|
+
new DevPortPrompt(),
|
|
566
|
+
new UseCasePrompt(),
|
|
567
|
+
new SignInPresetPrompt(),
|
|
568
|
+
new DesignPrompt()
|
|
409
569
|
];
|
|
410
570
|
//#endregion
|
|
411
571
|
//#region src/commands/setup/summary.ts
|
|
412
572
|
/**
|
|
573
|
+
* Renders a path under `cwd` with stable POSIX separators. Setup's summary
|
|
574
|
+
* matches generated artifacts against repository-style suffixes such as
|
|
575
|
+
* `.zitadel/schemas/default-human-user.json`; normalizing here keeps those
|
|
576
|
+
* matches working when Node returns Windows paths with backslashes.
|
|
577
|
+
*/
|
|
578
|
+
function relativeDisplayPath(cwd, absolute) {
|
|
579
|
+
const boundary = absolute[cwd.length];
|
|
580
|
+
return (absolute.startsWith(cwd) && (boundary === "/" || boundary === "\\") ? absolute.slice(cwd.length + 1) : absolute).replaceAll("\\", "/");
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
413
583
|
* Renders the section list as a single multi-line string. Labels are
|
|
414
584
|
* padded to a common width per section so the `✓ label value` columns
|
|
415
585
|
* line up, the title prints in dim gray, and `✓` is green. Values come
|
|
@@ -438,6 +608,8 @@ const path = (s) => pc.cyan(s);
|
|
|
438
608
|
const url = (s) => pc.cyan(s);
|
|
439
609
|
/** Yellow, for opaque ids the user shouldn't try to read. */
|
|
440
610
|
const id = (s) => pc.yellow(s);
|
|
611
|
+
/** Dim, for separators and secondary detail. */
|
|
612
|
+
const dim = (s) => pc.dim(s);
|
|
441
613
|
/**
|
|
442
614
|
* Reads the project root to identify the framework version, TS presence,
|
|
443
615
|
* and which package manager the user runs. Returns safe defaults (npm
|
|
@@ -498,6 +670,29 @@ function stripRange(range) {
|
|
|
498
670
|
function fileNameOf(p) {
|
|
499
671
|
return basename(p);
|
|
500
672
|
}
|
|
673
|
+
/**
|
|
674
|
+
* Layout caveats for the chosen login design, shown to humans after the
|
|
675
|
+
* summary box and carried in the JSON envelope's `warnings`.
|
|
676
|
+
*
|
|
677
|
+
* The split family's brand pane is keyed to the **login's own container**
|
|
678
|
+
* width — a `@container (max-width: 48rem)` query on the widget's mount, not
|
|
679
|
+
* a viewport media query. Above that the pane renders; below it the pane is
|
|
680
|
+
* `display: none` and the compact brand mark takes its place — and the split
|
|
681
|
+
* template only emits that mark when `branding.json` names `logo_url` or
|
|
682
|
+
* `hero_url`, so without one the narrow layout loses the branding entirely.
|
|
683
|
+
*
|
|
684
|
+
* Independent of posture, because the container query is: a full-page login
|
|
685
|
+
* hits the same collapse on a phone that an embedded card hits in a sidebar.
|
|
686
|
+
* Gating on `widget` would have been a guess about the container, and a wrong
|
|
687
|
+
* one in both directions — the wrapper setup scaffolds for a widget is
|
|
688
|
+
* full-width (the pane renders there), and a page-posture login on a phone is
|
|
689
|
+
* exactly the narrow case the advice is for. `hero` stays quiet: its compact
|
|
690
|
+
* fallback is editable text, so a narrow container never leaves it blank.
|
|
691
|
+
*/
|
|
692
|
+
function designWarnings(design) {
|
|
693
|
+
if (design !== "split" && design !== "split-right") return [];
|
|
694
|
+
return [`The ${design} design shows its brand pane only when the login's container is wide; a narrow container — a card-width embed, or any phone-width viewport — collapses it to the compact brand mark. Set logo_url (or hero_url) in .zitadel/branding/branding.json so that mark isn't empty.`];
|
|
695
|
+
}
|
|
501
696
|
//#endregion
|
|
502
697
|
//#region src/commands/setup/index.ts
|
|
503
698
|
/**
|
|
@@ -507,13 +702,23 @@ function fileNameOf(p) {
|
|
|
507
702
|
* load.
|
|
508
703
|
*/
|
|
509
704
|
const FRAMEWORK_OPTIONS = createOrca().availableFrameworks().map((framework) => framework.id);
|
|
705
|
+
/**
|
|
706
|
+
* `--renderer` offers only ids `getRenderer` will resolve: a
|
|
707
|
+
* declared-but-unpublished renderer (ADR 006) keeps its registry entry to
|
|
708
|
+
* reserve the id, but is surfaced as unavailable in the flag description
|
|
709
|
+
* instead of in `options`, so `--help` never advertises a value that is
|
|
710
|
+
* guaranteed to fail and an explicit pass is rejected at parse time — before
|
|
711
|
+
* any remote project is created.
|
|
712
|
+
*/
|
|
713
|
+
const UNAVAILABLE_RENDERER_IDS = RENDERER_IDS.filter((id) => !AVAILABLE_RENDERER_IDS.includes(id));
|
|
714
|
+
const RENDERER_FLAG_DESCRIPTION = UNAVAILABLE_RENDERER_IDS.length === 0 ? "Renderer (default: react)." : `Renderer (default: react). Not yet available: ${UNAVAILABLE_RENDERER_IDS.join(", ")}.`;
|
|
510
715
|
/** `zitadel setup` — create a project and scaffold local auth.
|
|
511
716
|
*
|
|
512
717
|
* Detects (or, for an empty directory, scaffolds then re-detects) the
|
|
513
718
|
* framework, runs the wizard prompts to fill in any answers not pre-supplied
|
|
514
|
-
* by flags, creates the remote project
|
|
515
|
-
*
|
|
516
|
-
*
|
|
719
|
+
* by flags, creates the remote project without server fallback defaults,
|
|
720
|
+
* patches the local files via `Orca`'s framework patcher, then scaffolds and
|
|
721
|
+
* uploads editable schema/flow config from `.zitadel/**`.
|
|
517
722
|
*
|
|
518
723
|
* Every interactive question lives in {@link SETUP_PROMPTS} (the main wizard
|
|
519
724
|
* — each entry is a small class) and {@link PickFrameworkPrompt} (the
|
|
@@ -528,18 +733,30 @@ var Setup = class Setup extends BaseCommand {
|
|
|
528
733
|
options: FRAMEWORK_OPTIONS
|
|
529
734
|
}),
|
|
530
735
|
renderer: Flags.string({
|
|
531
|
-
description:
|
|
532
|
-
options: [...
|
|
736
|
+
description: RENDERER_FLAG_DESCRIPTION,
|
|
737
|
+
options: [...AVAILABLE_RENDERER_IDS]
|
|
533
738
|
}),
|
|
534
739
|
"dev-port": Flags.integer({ description: "Dev-server port; also the issuer origin registered with Zitadel. Defaults to the detected port. Use distinct ports to run several scaffolded apps side by side." }),
|
|
535
|
-
"skip-install": Flags.boolean({ description: "Do not install dependencies after setup updates package.json." })
|
|
740
|
+
"skip-install": Flags.boolean({ description: "Do not install dependencies after setup updates package.json." }),
|
|
741
|
+
preset: Flags.string({
|
|
742
|
+
description: "Sign-in preset for the scaffolded schema and login flow (default: password-first).",
|
|
743
|
+
options: [...SETUP_PRESETS]
|
|
744
|
+
}),
|
|
745
|
+
"use-case": Flags.string({
|
|
746
|
+
description: "Use case for the scaffolded schema fields: who signs in to the app (default: minimal).",
|
|
747
|
+
options: [...SETUP_USE_CASES]
|
|
748
|
+
}),
|
|
749
|
+
design: Flags.string({
|
|
750
|
+
description: "Login design to eject into .zitadel/branding/ and publish as branding revision 1. Skips the wizard's design question. When omitted in non-interactive runs, the login uses the built-in template; run the `branding eject` command later to customize. Split-family designs (split, split-right, hero) collapse their brand pane by container width: narrow containers — including widget-posture embeds at card width — render the compact brand mark instead (logo_url, else hero_url, from .zitadel/branding/branding.json; hero falls back to editable text).",
|
|
751
|
+
options: [...BRANDING_DESIGNS]
|
|
752
|
+
})
|
|
536
753
|
};
|
|
537
754
|
async run() {
|
|
538
755
|
const { flags } = await this.parse(Setup);
|
|
539
756
|
try {
|
|
540
757
|
await this.toMeta(flags);
|
|
541
758
|
} catch (error) {
|
|
542
|
-
throw localSetupHint(error, flags
|
|
759
|
+
throw localSetupHint(error, retryOptionsFromFlags(flags), this.config.version);
|
|
543
760
|
}
|
|
544
761
|
const { cwd, nonInteractive, dryRun, force } = this.meta;
|
|
545
762
|
if (await hasZitadelConfig(cwd)) return this.emit({
|
|
@@ -564,6 +781,17 @@ var Setup = class Setup extends BaseCommand {
|
|
|
564
781
|
consola$1.success(`Scaffolded ${framework.id} skeleton`);
|
|
565
782
|
} else throw error;
|
|
566
783
|
}
|
|
784
|
+
this.recordTelemetry({
|
|
785
|
+
framework: framework.id,
|
|
786
|
+
renderer: flags.renderer ?? "react",
|
|
787
|
+
scaffolded_skeleton: scaffoldedFramework,
|
|
788
|
+
skip_install: Boolean(flags["skip-install"]),
|
|
789
|
+
dev_port_explicit: flags["dev-port"] !== void 0,
|
|
790
|
+
preset: flags.preset ?? DEFAULT_SETUP_PRESET,
|
|
791
|
+
use_case: flags["use-case"] ?? DEFAULT_SETUP_USE_CASE,
|
|
792
|
+
design: flags.design ?? "built-in",
|
|
793
|
+
step: "framework_resolved"
|
|
794
|
+
});
|
|
567
795
|
if (flags["dev-port"] !== void 0) {
|
|
568
796
|
const devPort = flags["dev-port"];
|
|
569
797
|
if (!Number.isInteger(devPort) || devPort < 1 || devPort > 65535) throw new ZitadelError("E_VALIDATION", `--dev-port must be an integer in 1..65535, got ${devPort}`);
|
|
@@ -575,18 +803,30 @@ var Setup = class Setup extends BaseCommand {
|
|
|
575
803
|
}
|
|
576
804
|
let answers = {
|
|
577
805
|
server: this.meta.source,
|
|
578
|
-
devPort: framework.devPort
|
|
806
|
+
devPort: framework.devPort,
|
|
807
|
+
preset: flags.preset ?? DEFAULT_SETUP_PRESET,
|
|
808
|
+
useCase: flags["use-case"] ?? DEFAULT_SETUP_USE_CASE,
|
|
809
|
+
design: flags.design
|
|
579
810
|
};
|
|
580
811
|
if (!nonInteractive && !dryRun) {
|
|
581
812
|
intro("Zitadel setup");
|
|
582
813
|
const promptCtx = {
|
|
583
814
|
framework,
|
|
815
|
+
cwd,
|
|
584
816
|
serverFlag: this.meta.serverFlag,
|
|
585
|
-
devPortFromFlag: flags["dev-port"] !== void 0
|
|
817
|
+
devPortFromFlag: flags["dev-port"] !== void 0,
|
|
818
|
+
presetFromFlag: flags.preset !== void 0,
|
|
819
|
+
useCaseFromFlag: flags["use-case"] !== void 0,
|
|
820
|
+
designFromFlag: flags.design !== void 0
|
|
586
821
|
};
|
|
587
822
|
for (const prompt of SETUP_PROMPTS) answers = await prompt.ask(answers, promptCtx);
|
|
588
823
|
outro("Configuration captured");
|
|
589
824
|
}
|
|
825
|
+
this.recordTelemetry({
|
|
826
|
+
preset: answers.preset,
|
|
827
|
+
use_case: answers.useCase,
|
|
828
|
+
design: answers.design ?? "built-in"
|
|
829
|
+
});
|
|
590
830
|
const issuer = issuerFromPort(answers.devPort);
|
|
591
831
|
framework = {
|
|
592
832
|
...framework,
|
|
@@ -595,8 +835,18 @@ var Setup = class Setup extends BaseCommand {
|
|
|
595
835
|
};
|
|
596
836
|
consola$1.start(`Creating project on ${answers.server}${dryRun ? " (dry run)" : ""}`);
|
|
597
837
|
const unauthClient = createZitadelClient({ baseUrl: answers.server });
|
|
598
|
-
const
|
|
838
|
+
const projectName = defaultProjectName(cwd, framework.id);
|
|
839
|
+
const project = dryRun ? dryRunProject(issuer) : await createProjectWithLocalHint(unauthClient, answers.server, this.meta.cliVersion, projectName, issuer, {
|
|
840
|
+
...retryOptionsFromFlags(flags),
|
|
841
|
+
framework: framework.id,
|
|
842
|
+
preset: answers.preset,
|
|
843
|
+
useCase: answers.useCase,
|
|
844
|
+
design: answers.design,
|
|
845
|
+
devPort: answers.devPort
|
|
846
|
+
});
|
|
599
847
|
consola$1.success(`Created project ${project.id}`);
|
|
848
|
+
this.recordTelemetry({ step: "project_created" });
|
|
849
|
+
const posture = derivePosture(framework.id, scaffoldedFramework);
|
|
600
850
|
const ctx = {
|
|
601
851
|
framework,
|
|
602
852
|
rendererId: flags.renderer ?? "react",
|
|
@@ -604,7 +854,10 @@ var Setup = class Setup extends BaseCommand {
|
|
|
604
854
|
issuer,
|
|
605
855
|
server: answers.server,
|
|
606
856
|
cliVersion: this.meta.cliVersion,
|
|
607
|
-
scaffoldedFramework
|
|
857
|
+
scaffoldedFramework,
|
|
858
|
+
posture,
|
|
859
|
+
preset: answers.preset,
|
|
860
|
+
useCase: answers.useCase
|
|
608
861
|
};
|
|
609
862
|
consola$1.start(`Patching project files${dryRun ? " (dry run)" : ""}`);
|
|
610
863
|
const result = await orca.patcherFor(framework.id).patch(ctx, {
|
|
@@ -613,12 +866,62 @@ var Setup = class Setup extends BaseCommand {
|
|
|
613
866
|
force
|
|
614
867
|
});
|
|
615
868
|
for (const file of result.filesWritten) {
|
|
616
|
-
const sentence = describeWrittenFile(
|
|
869
|
+
const sentence = describeWrittenFile(relativeDisplayPath(cwd, file), dryRun);
|
|
617
870
|
if (sentence) consola$1.info(sentence);
|
|
618
871
|
}
|
|
619
|
-
for (const file of result.filesSkipped) consola$1.info(`Left ${
|
|
620
|
-
|
|
872
|
+
for (const file of result.filesSkipped) consola$1.info(`Left ${relativeDisplayPath(cwd, file)} unchanged (already matches target)`);
|
|
873
|
+
let resourceResult;
|
|
874
|
+
try {
|
|
875
|
+
resourceResult = dryRun ? { filesWritten: [] } : await materializeSetupResources({
|
|
876
|
+
cwd,
|
|
877
|
+
client: createZitadelClient({
|
|
878
|
+
baseUrl: answers.server,
|
|
879
|
+
token: project.project_secret
|
|
880
|
+
}),
|
|
881
|
+
projectId: project.id,
|
|
882
|
+
force,
|
|
883
|
+
preset: answers.preset,
|
|
884
|
+
useCase: answers.useCase,
|
|
885
|
+
design: answers.design,
|
|
886
|
+
cliVersion: this.meta.cliVersion
|
|
887
|
+
});
|
|
888
|
+
} catch (error) {
|
|
889
|
+
await rm(join(cwd, "zitadel.json"), { force: true });
|
|
890
|
+
await rm(join(cwd, ".zitadel/secret"), { force: true });
|
|
891
|
+
const cause = toZitadelError(error);
|
|
892
|
+
throw new ZitadelError(cause.code, `Default resource setup failed: ${cause.message}`, {
|
|
893
|
+
hint: "The project was created but its default schema/flow upload did not finish. Re-run `zitadel setup` to start over (add --force to overwrite partially written .zitadel files).",
|
|
894
|
+
nextCommands: ["zitadel setup --force"],
|
|
895
|
+
details: cause.details
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
for (const file of resourceResult.filesWritten) {
|
|
899
|
+
const sentence = describeWrittenFile(relativeDisplayPath(cwd, file), dryRun);
|
|
900
|
+
if (sentence) consola$1.info(sentence);
|
|
901
|
+
}
|
|
902
|
+
const allFilesWritten = [...result.filesWritten, ...resourceResult.filesWritten];
|
|
903
|
+
consola$1.success(`Patched ${allFilesWritten.length} file${allFilesWritten.length === 1 ? "" : "s"}` + (result.filesSkipped.length > 0 ? ` (${result.filesSkipped.length} unchanged)` : ""));
|
|
904
|
+
this.recordTelemetry({
|
|
905
|
+
step: "files_patched",
|
|
906
|
+
files_written_count: allFilesWritten.length
|
|
907
|
+
});
|
|
908
|
+
if (!dryRun) try {
|
|
909
|
+
await writeScaffoldManifest({
|
|
910
|
+
cwd,
|
|
911
|
+
actions: orca.patcherFor(framework.id).artifacts({
|
|
912
|
+
framework,
|
|
913
|
+
rendererId: ctx.rendererId
|
|
914
|
+
}),
|
|
915
|
+
written: [...result.filesWritten, ...result.filesSkipped],
|
|
916
|
+
scaffoldedFramework,
|
|
917
|
+
devPort: answers.devPort,
|
|
918
|
+
posture
|
|
919
|
+
});
|
|
920
|
+
} catch (error) {
|
|
921
|
+
consola$1.debug("Failed to record the scaffold manifest", error);
|
|
922
|
+
}
|
|
621
923
|
const installOutcome = await installDependenciesForSetup({
|
|
924
|
+
cliVersion: this.meta.cliVersion,
|
|
622
925
|
cwd,
|
|
623
926
|
depsAdded: result.depsAdded,
|
|
624
927
|
dryRun,
|
|
@@ -628,7 +931,22 @@ var Setup = class Setup extends BaseCommand {
|
|
|
628
931
|
scaffoldedFramework,
|
|
629
932
|
skipInstall: Boolean(flags["skip-install"])
|
|
630
933
|
});
|
|
631
|
-
|
|
934
|
+
this.recordTelemetry({
|
|
935
|
+
step: "dependencies_installed",
|
|
936
|
+
package_manager: installOutcome.install.package_manager
|
|
937
|
+
});
|
|
938
|
+
const writtenRel = allFilesWritten.map((file) => relativeDisplayPath(cwd, file));
|
|
939
|
+
const claimNudge = claimState({
|
|
940
|
+
secret: {},
|
|
941
|
+
server: answers.server
|
|
942
|
+
}).kind === "detached" ? {
|
|
943
|
+
actions: [claimAction(this.meta.cliVersion)],
|
|
944
|
+
commands: [claimCommand(this.meta.cliVersion)]
|
|
945
|
+
} : {
|
|
946
|
+
actions: [],
|
|
947
|
+
commands: []
|
|
948
|
+
};
|
|
949
|
+
const warnings = designWarnings(answers.design);
|
|
632
950
|
if (!this.jsonEnabled()) {
|
|
633
951
|
const sections = buildSummary({
|
|
634
952
|
projectFacts: await detectProjectFacts(cwd, framework.id),
|
|
@@ -636,14 +954,15 @@ var Setup = class Setup extends BaseCommand {
|
|
|
636
954
|
project,
|
|
637
955
|
server: answers.server,
|
|
638
956
|
issuer,
|
|
639
|
-
scaffoldedFramework
|
|
957
|
+
scaffoldedFramework,
|
|
958
|
+
design: answers.design
|
|
640
959
|
});
|
|
641
960
|
consola$1.box({
|
|
642
961
|
title: "Zitadel is ready",
|
|
643
962
|
message: [
|
|
644
963
|
renderSummary(sections),
|
|
645
964
|
"",
|
|
646
|
-
installOutcome.
|
|
965
|
+
[...installOutcome.boxActions, ...claimNudge.actions].join("\n")
|
|
647
966
|
].join("\n"),
|
|
648
967
|
style: {
|
|
649
968
|
padding: 1,
|
|
@@ -651,9 +970,11 @@ var Setup = class Setup extends BaseCommand {
|
|
|
651
970
|
borderColor: "green"
|
|
652
971
|
}
|
|
653
972
|
});
|
|
973
|
+
for (const warning of warnings) consola$1.warn(warning);
|
|
654
974
|
}
|
|
655
975
|
return this.emit({
|
|
656
976
|
status: "ok",
|
|
977
|
+
warnings,
|
|
657
978
|
pretty: "",
|
|
658
979
|
data: {
|
|
659
980
|
title: "Zitadel is ready.",
|
|
@@ -663,11 +984,21 @@ var Setup = class Setup extends BaseCommand {
|
|
|
663
984
|
},
|
|
664
985
|
framework: framework.id,
|
|
665
986
|
server: answers.server,
|
|
666
|
-
files_written:
|
|
667
|
-
|
|
987
|
+
files_written: allFilesWritten.map((file) => relativeDisplayPath(cwd, file)),
|
|
988
|
+
files: result.files.map((file) => ({
|
|
989
|
+
path: relativeDisplayPath(cwd, file.path),
|
|
990
|
+
kind: file.kind,
|
|
991
|
+
action: file.action
|
|
992
|
+
})),
|
|
993
|
+
files_skipped: result.filesSkipped.map((file) => relativeDisplayPath(cwd, file)),
|
|
668
994
|
install: installOutcome.install,
|
|
669
|
-
|
|
670
|
-
|
|
995
|
+
design: answers.design ?? null,
|
|
996
|
+
next_actions: [
|
|
997
|
+
...installOutcome.nextActions,
|
|
998
|
+
brandingGuidanceAction(answers.design, this.meta.cliVersion),
|
|
999
|
+
...claimNudge.actions
|
|
1000
|
+
],
|
|
1001
|
+
next_commands: [...installOutcome.nextCommands, ...claimNudge.commands]
|
|
671
1002
|
}
|
|
672
1003
|
});
|
|
673
1004
|
}
|
|
@@ -696,20 +1027,59 @@ async function resolveScaffoldFramework(framework, nonInteractive, orca) {
|
|
|
696
1027
|
function dryRunProject(issuer) {
|
|
697
1028
|
return {
|
|
698
1029
|
id: "dry-run-0000",
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
1030
|
+
project_secret: "sk_proj_dry_run_full",
|
|
1031
|
+
preview_secret: "sk_proj_dry_run_preview",
|
|
1032
|
+
preview_origins: [issuer],
|
|
1033
|
+
created_at: "2026-04-21T14:03:11.000Z"
|
|
703
1034
|
};
|
|
704
1035
|
}
|
|
705
|
-
|
|
1036
|
+
/**
|
|
1037
|
+
* Reconstructs the flag list of the current invocation for retry guidance,
|
|
1038
|
+
* ending in `--server local`. Flags whose resolved value equals the default
|
|
1039
|
+
* are omitted — the retry reproduces the same outcome without them.
|
|
1040
|
+
*/
|
|
1041
|
+
function setupRetryFlags(opts) {
|
|
1042
|
+
const parts = [];
|
|
1043
|
+
if (opts.framework) parts.push(`--framework ${opts.framework}`);
|
|
1044
|
+
if (opts.preset && opts.preset !== DEFAULT_SETUP_PRESET) parts.push(`--preset ${opts.preset}`);
|
|
1045
|
+
if (opts.useCase && opts.useCase !== DEFAULT_SETUP_USE_CASE) parts.push(`--use-case ${opts.useCase}`);
|
|
1046
|
+
if (opts.design) parts.push(`--design ${opts.design}`);
|
|
1047
|
+
if (opts.renderer && opts.renderer !== "react") parts.push(`--renderer ${opts.renderer}`);
|
|
1048
|
+
if (opts.devPort !== void 0) parts.push(`--dev-port ${opts.devPort}`);
|
|
1049
|
+
if (opts.nonInteractive) parts.push("--non-interactive");
|
|
1050
|
+
parts.push("--server local");
|
|
1051
|
+
return parts.join(" ");
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Retry options straight from parsed flags, for failures before the wizard
|
|
1055
|
+
* resolves answers. `--non-interactive` is echoed only when explicitly passed
|
|
1056
|
+
* — TTY/JSON-inferred non-interactivity re-infers itself on the retry.
|
|
1057
|
+
*/
|
|
1058
|
+
function retryOptionsFromFlags(flags) {
|
|
1059
|
+
return {
|
|
1060
|
+
framework: flags.framework,
|
|
1061
|
+
preset: flags.preset,
|
|
1062
|
+
useCase: flags["use-case"],
|
|
1063
|
+
design: flags.design,
|
|
1064
|
+
renderer: flags.renderer,
|
|
1065
|
+
devPort: flags["dev-port"],
|
|
1066
|
+
nonInteractive: Boolean(flags["non-interactive"])
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
async function createProjectWithLocalHint(client, server, cliVersion, projectName, issuer, retry) {
|
|
706
1070
|
try {
|
|
707
|
-
|
|
1071
|
+
const payload = {
|
|
1072
|
+
name: projectName,
|
|
1073
|
+
preview_origins: [issuer],
|
|
1074
|
+
seed_defaults: false
|
|
1075
|
+
};
|
|
1076
|
+
return await client.createProject(payload);
|
|
708
1077
|
} catch (error) {
|
|
709
1078
|
const normalized = toZitadelError(error);
|
|
1079
|
+
const retryFlags = setupRetryFlags(retry);
|
|
710
1080
|
throw new ZitadelError(normalized.code, normalized.message, {
|
|
711
|
-
hint: `${normalized.hint ? `${normalized.hint} ` : ""}If you meant to use a local Zitadel server, start it first and retry setup with
|
|
712
|
-
nextCommands: [publicCliCommand("start", cliVersion), publicCliCommand(`setup
|
|
1081
|
+
hint: `${normalized.hint ? `${normalized.hint} ` : ""}If you meant to use a local Zitadel server, start it first and retry setup with ${retryFlags}.`,
|
|
1082
|
+
nextCommands: [publicCliCommand("start", cliVersion), publicCliCommand(`setup ${retryFlags}`, cliVersion)],
|
|
713
1083
|
details: {
|
|
714
1084
|
server,
|
|
715
1085
|
original: normalized.details
|
|
@@ -717,20 +1087,20 @@ async function createProjectWithLocalHint(client, server, cliVersion, issuer, fr
|
|
|
717
1087
|
});
|
|
718
1088
|
}
|
|
719
1089
|
}
|
|
720
|
-
function
|
|
1090
|
+
function defaultProjectName(cwd, framework) {
|
|
1091
|
+
const fromDirectory = basename(cwd).trim();
|
|
1092
|
+
return fromDirectory.length > 0 ? fromDirectory : `zitadel-${framework}-app`;
|
|
1093
|
+
}
|
|
1094
|
+
function localSetupHint(error, retry, cliVersion) {
|
|
721
1095
|
const normalized = toZitadelError(error);
|
|
722
1096
|
if (normalized.code !== "E_LOCAL_SERVER_NOT_RUNNING") return error;
|
|
723
|
-
const setupCommand =
|
|
1097
|
+
const setupCommand = `setup ${setupRetryFlags(retry)}`;
|
|
724
1098
|
return new ZitadelError(normalized.code, normalized.message, {
|
|
725
1099
|
hint: `${normalized.hint ? `${normalized.hint} ` : ""}Start local Zitadel first, then rerun setup. After setup succeeds, follow its next_commands to start the app and verify registration, logout, and login in the browser.`,
|
|
726
1100
|
nextCommands: [publicCliCommand("start", cliVersion), publicCliCommand(setupCommand, cliVersion)],
|
|
727
1101
|
details: normalized.details
|
|
728
1102
|
});
|
|
729
1103
|
}
|
|
730
|
-
/** Renders an absolute path relative to `cwd` for human-readable output. */
|
|
731
|
-
function relativeDisplay(cwd, path) {
|
|
732
|
-
return path.startsWith(cwd) ? path.slice(cwd.length + 1) : path;
|
|
733
|
-
}
|
|
734
1104
|
/**
|
|
735
1105
|
* Replaces the user's `$HOME` with `~` in a path for compact terminal output.
|
|
736
1106
|
* Falls back to the raw path when `HOME` isn't set or doesn't match.
|
|
@@ -751,15 +1121,13 @@ function pickWrittenFile(written, suffix) {
|
|
|
751
1121
|
}
|
|
752
1122
|
/**
|
|
753
1123
|
* Translates a patcher-written path into a single sentence the user can
|
|
754
|
-
* read at narration speed.
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
*
|
|
758
|
-
*
|
|
759
|
-
* something happened.
|
|
1124
|
+
* read at narration speed. The patch result's `filesWritten` carries
|
|
1125
|
+
* deduplicated file paths only (directories stay in the typed `files`
|
|
1126
|
+
* rows), so no artefact filtering is needed here. The verb tense flips
|
|
1127
|
+
* for `--dry-run` so the user sees a preview ("Would write ...") instead
|
|
1128
|
+
* of a claim that something happened.
|
|
760
1129
|
*/
|
|
761
1130
|
function describeWrittenFile(relPath, dryRun) {
|
|
762
|
-
if (relPath === ".zitadel" || relPath === ".zitadel/flows" || relPath === ".zitadel/schemas") return null;
|
|
763
1131
|
const verb = dryRun ? "Would write" : "Wrote";
|
|
764
1132
|
const sentence = SENTENCE_BY_PATH[relPath];
|
|
765
1133
|
if (sentence) return `${verb} ${sentence.subject} (${path(relPath)})`;
|
|
@@ -777,8 +1145,21 @@ const SENTENCE_BY_PATH = {
|
|
|
777
1145
|
"zitadel.json": { subject: "the Zitadel project configuration" },
|
|
778
1146
|
".env.example": { subject: "the .env example template" },
|
|
779
1147
|
".env.local": { subject: "the local development environment variables" },
|
|
780
|
-
".zitadel/state.json": { subject: "the
|
|
781
|
-
"
|
|
1148
|
+
".zitadel/state.json": { subject: "the sync state file" },
|
|
1149
|
+
".zitadel/flows/default-login.json": { subject: "the editable default login flow" },
|
|
1150
|
+
".zitadel/flows/README.md": { subject: "the flows folder README" },
|
|
1151
|
+
".zitadel/schemas/default-human-user.json": { subject: "the editable default human user schema" },
|
|
1152
|
+
".zitadel/schemas/README.md": { subject: "the schemas folder README" },
|
|
1153
|
+
".zitadel/meta/flow-definition.json": { subject: "the flow dialect spec (editor $schema)" },
|
|
1154
|
+
".zitadel/meta/user-schema.json": { subject: "the user-schema dialect spec" },
|
|
1155
|
+
".zitadel/meta/user-property.json": { subject: "the user-property dialect spec" },
|
|
1156
|
+
".zitadel/meta/branding.json": { subject: "the branding dialect spec" },
|
|
1157
|
+
".zitadel/branding/branding.json": { subject: "the branding descriptor (layout + asset URLs)" },
|
|
1158
|
+
".zitadel/branding/login.liquid": { subject: "the editable login template" },
|
|
1159
|
+
".zitadel/branding/README.md": { subject: "the branding folder README" },
|
|
1160
|
+
"AGENTS.md": { subject: "the agent guidance (golden journey + config dialect)" },
|
|
1161
|
+
"README.md": { subject: "the README's Zitadel section" },
|
|
1162
|
+
"app/page.tsx": { subject: "the home page redirect" },
|
|
782
1163
|
"app/login/page.tsx": { subject: "the login page" },
|
|
783
1164
|
"app/register/page.tsx": { subject: "the registration page" },
|
|
784
1165
|
"app/profile/page.tsx": { subject: "the profile page" },
|
|
@@ -789,7 +1170,7 @@ const SENTENCE_BY_PATH = {
|
|
|
789
1170
|
};
|
|
790
1171
|
/** Builds the section list driving {@link renderSummary} for the setup command. */
|
|
791
1172
|
function buildSummary(opts) {
|
|
792
|
-
const { projectFacts, writtenRel, project, server, issuer, scaffoldedFramework } = opts;
|
|
1173
|
+
const { projectFacts, writtenRel, project, server, issuer, scaffoldedFramework, design } = opts;
|
|
793
1174
|
const sdkPackage = "@zitadel/sdk-next";
|
|
794
1175
|
const packageJsonHit = pickWrittenFile(writtenRel, "package.json");
|
|
795
1176
|
const detected = [{
|
|
@@ -807,7 +1188,7 @@ function buildSummary(opts) {
|
|
|
807
1188
|
secondary: path(fileNameOf(packageJsonHit))
|
|
808
1189
|
});
|
|
809
1190
|
for (const [label, suffix] of [
|
|
810
|
-
["Home
|
|
1191
|
+
["Home redirect", "app/page.tsx"],
|
|
811
1192
|
["Login page", "app/login/page.tsx"],
|
|
812
1193
|
["Register page", "app/register/page.tsx"],
|
|
813
1194
|
["Profile page", "app/profile/page.tsx"],
|
|
@@ -821,6 +1202,28 @@ function buildSummary(opts) {
|
|
|
821
1202
|
value: path(hit)
|
|
822
1203
|
});
|
|
823
1204
|
}
|
|
1205
|
+
const customizeRows = [];
|
|
1206
|
+
for (const [label, suffix, dir] of [
|
|
1207
|
+
[
|
|
1208
|
+
"User schema",
|
|
1209
|
+
".zitadel/schemas/default-human-user.json",
|
|
1210
|
+
".zitadel/schemas/"
|
|
1211
|
+
],
|
|
1212
|
+
[
|
|
1213
|
+
"Login flow",
|
|
1214
|
+
".zitadel/flows/default-login.json",
|
|
1215
|
+
".zitadel/flows/"
|
|
1216
|
+
],
|
|
1217
|
+
[
|
|
1218
|
+
"Login template",
|
|
1219
|
+
".zitadel/branding/login.liquid",
|
|
1220
|
+
".zitadel/branding/"
|
|
1221
|
+
]
|
|
1222
|
+
]) if (pickWrittenFile(writtenRel, suffix)) customizeRows.push({
|
|
1223
|
+
label,
|
|
1224
|
+
value: path(dir),
|
|
1225
|
+
secondary: "see its README.md"
|
|
1226
|
+
});
|
|
824
1227
|
const projectRows = [
|
|
825
1228
|
{
|
|
826
1229
|
label: "Project id",
|
|
@@ -833,6 +1236,14 @@ function buildSummary(opts) {
|
|
|
833
1236
|
{
|
|
834
1237
|
label: "App will run",
|
|
835
1238
|
value: url(issuer)
|
|
1239
|
+
},
|
|
1240
|
+
design ? {
|
|
1241
|
+
label: "Login design",
|
|
1242
|
+
value: brandingDesignLabel(design),
|
|
1243
|
+
secondary: `${design} · ${path(".zitadel/branding/")}`
|
|
1244
|
+
} : {
|
|
1245
|
+
label: "Login design",
|
|
1246
|
+
value: dim("built-in template")
|
|
836
1247
|
}
|
|
837
1248
|
];
|
|
838
1249
|
return [
|
|
@@ -844,6 +1255,10 @@ function buildSummary(opts) {
|
|
|
844
1255
|
title: "Installed",
|
|
845
1256
|
rows: installedRows
|
|
846
1257
|
},
|
|
1258
|
+
{
|
|
1259
|
+
title: "Customize",
|
|
1260
|
+
rows: customizeRows
|
|
1261
|
+
},
|
|
847
1262
|
{
|
|
848
1263
|
title: "Project",
|
|
849
1264
|
rows: projectRows
|