@zitadel/cli 0.1.0-alpha.8 → 1.0.0-alpha.20
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 +227 -38
- 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 +402 -0
- package/dist/commands/claim.mjs.map +1 -0
- package/dist/commands/doctor.mjs +687 -55
- 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 +12 -4
- package/dist/commands/reset.mjs.map +1 -1
- package/dist/commands/schemas/list.mjs +146 -0
- package/dist/commands/schemas/list.mjs.map +1 -0
- package/dist/commands/setup.mjs +642 -287
- package/dist/commands/setup.mjs.map +1 -1
- package/dist/commands/start.mjs +73 -6
- 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 +59 -6
- 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-BA78SdC2.mjs → docker-DcRGTOJa.mjs} +61 -11
- package/dist/docker-DcRGTOJa.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-CfKDQRop.mjs → orca-BzQIQqzt.mjs} +1663 -303
- package/dist/orca-BzQIQqzt.mjs.map +1 -0
- package/dist/package-manager-DFdLcDx1.mjs +194 -0
- package/dist/package-manager-DFdLcDx1.mjs.map +1 -0
- package/dist/ports-BM20XIZb.mjs +116 -0
- package/dist/ports-BM20XIZb.mjs.map +1 -0
- package/dist/processes-DKHPkU8O.mjs +120 -0
- package/dist/processes-DKHPkU8O.mjs.map +1 -0
- package/dist/{project-CKAHtHML.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 +412 -5
- package/package.json +10 -5
- package/dist/docker-BA78SdC2.mjs.map +0 -1
- package/dist/docker-guidance-BvfpmsDj.mjs +0 -21
- package/dist/docker-guidance-BvfpmsDj.mjs.map +0 -1
- package/dist/oclif-VkCTGIEk.mjs +0 -818
- package/dist/oclif-VkCTGIEk.mjs.map +0 -1
- package/dist/orca-CfKDQRop.mjs.map +0 -1
- package/dist/project-CKAHtHML.mjs.map +0 -1
- package/dist/sync-B5lqgQO3.mjs +0 -733
- package/dist/sync-B5lqgQO3.mjs.map +0 -1
package/dist/commands/setup.mjs
CHANGED
|
@@ -1,95 +1,181 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { n 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-BzQIQqzt.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";
|
|
4
13
|
import { cancel, confirm, intro, isCancel, outro, select, spinner, text } from "@clack/prompts";
|
|
5
14
|
import { Flags } from "@oclif/core";
|
|
6
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";
|
|
7
17
|
import { consola as consola$1 } from "consola";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { execFile, spawn } from "node:child_process";
|
|
18
|
+
import { BRANDING_FILE_SCHEMA_REF } from "@zitadel/config/meta-schemas";
|
|
19
|
+
import { normalizeFlowBody, normalizeSchemaBody } from "@zitadel/config/normalize";
|
|
11
20
|
import pc from "picocolors";
|
|
12
|
-
//#region src/lib/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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";
|
|
33
47
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const error = /* @__PURE__ */ new Error(`${packageCommand.display} failed with ${detail}`);
|
|
57
|
-
Object.assign(error, {
|
|
58
|
-
code: code ?? 1,
|
|
59
|
-
signal
|
|
60
|
-
});
|
|
61
|
-
reject(error);
|
|
62
|
-
});
|
|
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
|
|
63
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 };
|
|
64
129
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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 });
|
|
73
138
|
try {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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;
|
|
79
144
|
}
|
|
80
145
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
+
}
|
|
84
161
|
}
|
|
85
|
-
async function
|
|
162
|
+
async function writeResourceFile(cwd, relPath, body, force) {
|
|
163
|
+
const contents = `${stableStringify(body)}\n`;
|
|
86
164
|
try {
|
|
87
|
-
await
|
|
165
|
+
await writeFile(join(cwd, relPath), contents, force ? void 0 : { flag: "wx" });
|
|
88
166
|
return true;
|
|
89
|
-
} catch {
|
|
90
|
-
|
|
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;
|
|
91
170
|
}
|
|
92
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
|
+
}
|
|
93
179
|
//#endregion
|
|
94
180
|
//#region src/commands/setup/install.ts
|
|
95
181
|
async function installDependenciesForSetup(input) {
|
|
@@ -104,6 +190,7 @@ async function installDependenciesForSetup(input) {
|
|
|
104
190
|
reason: "no-dependency-changes"
|
|
105
191
|
},
|
|
106
192
|
devCommand: devCommand.display,
|
|
193
|
+
cliVersion: input.cliVersion,
|
|
107
194
|
issuer: input.issuer,
|
|
108
195
|
includeInstallCommand: false
|
|
109
196
|
});
|
|
@@ -115,6 +202,7 @@ async function installDependenciesForSetup(input) {
|
|
|
115
202
|
reason: input.dryRun ? "dry-run" : "skip-install"
|
|
116
203
|
},
|
|
117
204
|
devCommand: devCommand.display,
|
|
205
|
+
cliVersion: input.cliVersion,
|
|
118
206
|
issuer: input.issuer,
|
|
119
207
|
includeInstallCommand: true
|
|
120
208
|
});
|
|
@@ -136,22 +224,30 @@ async function installDependenciesForSetup(input) {
|
|
|
136
224
|
command: installCommand.display
|
|
137
225
|
},
|
|
138
226
|
devCommand: devCommand.display,
|
|
227
|
+
cliVersion: input.cliVersion,
|
|
139
228
|
issuer: input.issuer,
|
|
140
229
|
includeInstallCommand: false
|
|
141
230
|
});
|
|
142
231
|
}
|
|
143
232
|
function outcome(input) {
|
|
144
|
-
const
|
|
145
|
-
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.`;
|
|
146
241
|
return {
|
|
147
242
|
install: input.install,
|
|
148
243
|
devCommand: input.devCommand,
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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]
|
|
155
251
|
};
|
|
156
252
|
}
|
|
157
253
|
function installFailed(error, installCommand, devCommand, cwd) {
|
|
@@ -187,6 +283,54 @@ function bail(value) {
|
|
|
187
283
|
}
|
|
188
284
|
}
|
|
189
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
|
|
190
334
|
//#region src/commands/setup/prompts/dev-port.ts
|
|
191
335
|
/**
|
|
192
336
|
* "Dev server port" — defaults to the detected port. The validated answer
|
|
@@ -230,137 +374,27 @@ var FrameworkConfirmPrompt = class {
|
|
|
230
374
|
}
|
|
231
375
|
};
|
|
232
376
|
//#endregion
|
|
233
|
-
//#region src/lib/prober/ports.ts
|
|
234
|
-
/**
|
|
235
|
-
* Enumerate TCP ports currently in LISTEN state on the loopback interface
|
|
236
|
-
* (`127.0.0.1`, `::1`, or the wildcard `*`). Spawns `lsof -iTCP -sTCP:LISTEN
|
|
237
|
-
* -P -n -F n` and parses its machine-readable output. Returns the unique,
|
|
238
|
-
* numerically-sorted list of ports.
|
|
239
|
-
*
|
|
240
|
-
* Never throws. Returns `[]` whenever lsof is unavailable (e.g. Windows,
|
|
241
|
-
* unusual PATH), the spawn errors, exits non-zero, or the call exceeds
|
|
242
|
-
* `timeoutMs` (default 1000ms). The caller treats an empty list the same as
|
|
243
|
-
* "no listeners worth probing."
|
|
244
|
-
*/
|
|
245
|
-
async function listListeningPorts(opts) {
|
|
246
|
-
const timeoutMs = opts?.timeoutMs ?? 1e3;
|
|
247
|
-
try {
|
|
248
|
-
return parseLsofPorts(await runLsof(timeoutMs));
|
|
249
|
-
} catch {
|
|
250
|
-
return [];
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Spawn `lsof` with the canned argv and resolve to its stdout. Hand-rolled
|
|
255
|
-
* rather than `util.promisify(execFile)` because the latter resolves with a
|
|
256
|
-
* `{stdout, stderr}` object via its custom-promisify symbol — a heavier shape
|
|
257
|
-
* to mock and worse to read at the call site, where we only ever want stdout.
|
|
258
|
-
*/
|
|
259
|
-
function runLsof(timeoutMs) {
|
|
260
|
-
return new Promise((resolve, reject) => {
|
|
261
|
-
execFile("lsof", [
|
|
262
|
-
"-iTCP",
|
|
263
|
-
"-sTCP:LISTEN",
|
|
264
|
-
"-P",
|
|
265
|
-
"-n",
|
|
266
|
-
"-F",
|
|
267
|
-
"n"
|
|
268
|
-
], {
|
|
269
|
-
timeout: timeoutMs,
|
|
270
|
-
encoding: "utf8"
|
|
271
|
-
}, (err, stdout) => {
|
|
272
|
-
if (err) {
|
|
273
|
-
reject(err);
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
resolve(stdout);
|
|
277
|
-
});
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
/**
|
|
281
|
-
* Parse the `n` records emitted by `lsof -F n`. Each record is a single line
|
|
282
|
-
* `n<address>` where `<address>` ends in `:<port>` (e.g. `n*:8080`,
|
|
283
|
-
* `n127.0.0.1:3000`, `n[::1]:5050`). Only loopback/wildcard hosts are kept;
|
|
284
|
-
* external interface bindings are ignored.
|
|
285
|
-
*/
|
|
286
|
-
function parseLsofPorts(stdout) {
|
|
287
|
-
const ports = /* @__PURE__ */ new Set();
|
|
288
|
-
for (const line of stdout.split(/\r?\n/)) {
|
|
289
|
-
if (!line.startsWith("n")) continue;
|
|
290
|
-
const address = line.slice(1);
|
|
291
|
-
const colon = address.lastIndexOf(":");
|
|
292
|
-
if (colon < 0) continue;
|
|
293
|
-
const host = address.slice(0, colon);
|
|
294
|
-
const portStr = address.slice(colon + 1);
|
|
295
|
-
if (!isLoopback(host)) continue;
|
|
296
|
-
const port = Number.parseInt(portStr, 10);
|
|
297
|
-
if (Number.isFinite(port) && port > 0 && port < 65536) ports.add(port);
|
|
298
|
-
}
|
|
299
|
-
return [...ports].sort((a, b) => a - b);
|
|
300
|
-
}
|
|
301
|
-
/** Recognised loopback host strings as emitted by `lsof -F n`. */
|
|
302
|
-
function isLoopback(host) {
|
|
303
|
-
return host === "*" || host === "127.0.0.1" || host === "[::1]" || host === "::1";
|
|
304
|
-
}
|
|
305
|
-
//#endregion
|
|
306
|
-
//#region src/lib/prober/http.ts
|
|
307
|
-
/**
|
|
308
|
-
* Fetch `url` with a per-call timeout and pass the resulting `Response` to
|
|
309
|
-
* `predicate`. Returns the predicate's value, or `null` on any failure: a
|
|
310
|
-
* network error, an `AbortError` from the timeout, a thrown predicate, or the
|
|
311
|
-
* predicate returning `null`.
|
|
312
|
-
*
|
|
313
|
-
* Never throws. The predicate decides what counts as a match — it gets the
|
|
314
|
-
* raw `Response` and may read `.json()` / `.headers` / `.status` as needed.
|
|
315
|
-
*
|
|
316
|
-
* `timeoutMs` defaults to 500ms. The implementation uses
|
|
317
|
-
* `AbortSignal.timeout`, so the underlying fetch is cancelled when the
|
|
318
|
-
* timeout fires (no orphaned sockets).
|
|
319
|
-
*/
|
|
320
|
-
async function probeUrl(url, predicate, opts) {
|
|
321
|
-
const timeoutMs = opts?.timeoutMs ?? 500;
|
|
322
|
-
try {
|
|
323
|
-
return await predicate(await fetch(url, { signal: AbortSignal.timeout(timeoutMs) }));
|
|
324
|
-
} catch {
|
|
325
|
-
return null;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
/**
|
|
329
|
-
* Probe many URLs in parallel and return the non-null matches, preserving
|
|
330
|
-
* the input order. Each URL is bounded by `timeoutMs` independently — one
|
|
331
|
-
* slow target can't hold up the others.
|
|
332
|
-
*/
|
|
333
|
-
async function probeUrls(urls, predicate, opts) {
|
|
334
|
-
const list = [...urls];
|
|
335
|
-
return (await Promise.all(list.map(async (url) => {
|
|
336
|
-
const value = await probeUrl(url, predicate, opts);
|
|
337
|
-
return value === null ? null : {
|
|
338
|
-
url,
|
|
339
|
-
value
|
|
340
|
-
};
|
|
341
|
-
}))).filter((match) => match !== null);
|
|
342
|
-
}
|
|
343
|
-
//#endregion
|
|
344
377
|
//#region src/commands/setup/prompts/server.ts
|
|
345
378
|
/** Sentinel returned by the choice select when the user picks "Custom URL". */
|
|
346
379
|
const CUSTOM = "__custom__";
|
|
347
|
-
/** Per-probe timeout for the localhost OIDC scan — generous enough for a TLS handshake on a busy laptop. */
|
|
348
|
-
const OIDC_PROBE_TIMEOUT_MS = 300;
|
|
349
380
|
/**
|
|
350
|
-
* "Which server should `zitadel.json` point to?" — Zitadel Cloud,
|
|
351
|
-
*
|
|
381
|
+
* "Which server should `zitadel.json` point to?" — Zitadel Cloud, the managed
|
|
382
|
+
* local server we detected, or a custom URL.
|
|
352
383
|
*
|
|
353
|
-
* Before asking, it
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
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
|
|
358
392
|
* validated URL exactly as before.
|
|
359
393
|
*/
|
|
360
394
|
var ServerPrompt = class {
|
|
361
395
|
async ask(answers, ctx) {
|
|
362
396
|
if (ctx.serverFlag) return answers;
|
|
363
|
-
const
|
|
397
|
+
const detected = await detectLocalServer(ctx.cwd);
|
|
364
398
|
const choice = await select({
|
|
365
399
|
message: "Which server should zitadel.json point to?",
|
|
366
400
|
options: [
|
|
@@ -369,17 +403,17 @@ var ServerPrompt = class {
|
|
|
369
403
|
label: "Zitadel Cloud (api.zitadel.cloud)",
|
|
370
404
|
hint: "recommended for real projects"
|
|
371
405
|
},
|
|
372
|
-
...
|
|
373
|
-
value:
|
|
374
|
-
label: server
|
|
375
|
-
hint: "detected —
|
|
376
|
-
}
|
|
406
|
+
...detected ? [{
|
|
407
|
+
value: detected,
|
|
408
|
+
label: `Local Zitadel server (${detected})`,
|
|
409
|
+
hint: "detected — started with zitadel start"
|
|
410
|
+
}] : [],
|
|
377
411
|
{
|
|
378
412
|
value: CUSTOM,
|
|
379
413
|
label: "Custom URL (self-hosted)"
|
|
380
414
|
}
|
|
381
415
|
],
|
|
382
|
-
initialValue: answers.server ?? "https://api.zitadel.cloud"
|
|
416
|
+
initialValue: detected ?? answers.server ?? "https://api.zitadel.cloud"
|
|
383
417
|
});
|
|
384
418
|
bail(choice);
|
|
385
419
|
if (choice !== CUSTOM) return {
|
|
@@ -406,38 +440,89 @@ var ServerPrompt = class {
|
|
|
406
440
|
}
|
|
407
441
|
};
|
|
408
442
|
/**
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
* Zitadel-shaped detail here is the OIDC predicate, which is the standard
|
|
413
|
-
* "any OIDC server" contract (no `zitadel`-keyword check), so Keycloak/dex/
|
|
414
|
-
* 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.
|
|
415
446
|
*/
|
|
416
|
-
async function
|
|
447
|
+
async function detectLocalServer(cwd) {
|
|
417
448
|
const s = spinner();
|
|
418
|
-
s.start("
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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"
|
|
432
508
|
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
+
};
|
|
439
524
|
}
|
|
440
|
-
}
|
|
525
|
+
};
|
|
441
526
|
//#endregion
|
|
442
527
|
//#region src/commands/setup/prompts/pick-framework.ts
|
|
443
528
|
/**
|
|
@@ -477,11 +562,24 @@ var PickFrameworkPrompt = class {
|
|
|
477
562
|
const SETUP_PROMPTS = [
|
|
478
563
|
new FrameworkConfirmPrompt(),
|
|
479
564
|
new ServerPrompt(),
|
|
480
|
-
new DevPortPrompt()
|
|
565
|
+
new DevPortPrompt(),
|
|
566
|
+
new UseCasePrompt(),
|
|
567
|
+
new SignInPresetPrompt(),
|
|
568
|
+
new DesignPrompt()
|
|
481
569
|
];
|
|
482
570
|
//#endregion
|
|
483
571
|
//#region src/commands/setup/summary.ts
|
|
484
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
|
+
/**
|
|
485
583
|
* Renders the section list as a single multi-line string. Labels are
|
|
486
584
|
* padded to a common width per section so the `✓ label value` columns
|
|
487
585
|
* line up, the title prints in dim gray, and `✓` is green. Values come
|
|
@@ -510,6 +608,8 @@ const path = (s) => pc.cyan(s);
|
|
|
510
608
|
const url = (s) => pc.cyan(s);
|
|
511
609
|
/** Yellow, for opaque ids the user shouldn't try to read. */
|
|
512
610
|
const id = (s) => pc.yellow(s);
|
|
611
|
+
/** Dim, for separators and secondary detail. */
|
|
612
|
+
const dim = (s) => pc.dim(s);
|
|
513
613
|
/**
|
|
514
614
|
* Reads the project root to identify the framework version, TS presence,
|
|
515
615
|
* and which package manager the user runs. Returns safe defaults (npm
|
|
@@ -570,6 +670,29 @@ function stripRange(range) {
|
|
|
570
670
|
function fileNameOf(p) {
|
|
571
671
|
return basename(p);
|
|
572
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
|
+
}
|
|
573
696
|
//#endregion
|
|
574
697
|
//#region src/commands/setup/index.ts
|
|
575
698
|
/**
|
|
@@ -579,13 +702,23 @@ function fileNameOf(p) {
|
|
|
579
702
|
* load.
|
|
580
703
|
*/
|
|
581
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(", ")}.`;
|
|
582
715
|
/** `zitadel setup` — create a project and scaffold local auth.
|
|
583
716
|
*
|
|
584
717
|
* Detects (or, for an empty directory, scaffolds then re-detects) the
|
|
585
718
|
* framework, runs the wizard prompts to fill in any answers not pre-supplied
|
|
586
|
-
* by flags, creates the remote project
|
|
587
|
-
*
|
|
588
|
-
*
|
|
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/**`.
|
|
589
722
|
*
|
|
590
723
|
* Every interactive question lives in {@link SETUP_PROMPTS} (the main wizard
|
|
591
724
|
* — each entry is a small class) and {@link PickFrameworkPrompt} (the
|
|
@@ -600,18 +733,30 @@ var Setup = class Setup extends BaseCommand {
|
|
|
600
733
|
options: FRAMEWORK_OPTIONS
|
|
601
734
|
}),
|
|
602
735
|
renderer: Flags.string({
|
|
603
|
-
description:
|
|
604
|
-
options: [...
|
|
736
|
+
description: RENDERER_FLAG_DESCRIPTION,
|
|
737
|
+
options: [...AVAILABLE_RENDERER_IDS]
|
|
605
738
|
}),
|
|
606
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." }),
|
|
607
|
-
"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
|
+
})
|
|
608
753
|
};
|
|
609
754
|
async run() {
|
|
610
755
|
const { flags } = await this.parse(Setup);
|
|
611
756
|
try {
|
|
612
757
|
await this.toMeta(flags);
|
|
613
758
|
} catch (error) {
|
|
614
|
-
throw localSetupHint(error, flags
|
|
759
|
+
throw localSetupHint(error, retryOptionsFromFlags(flags), this.config.version);
|
|
615
760
|
}
|
|
616
761
|
const { cwd, nonInteractive, dryRun, force } = this.meta;
|
|
617
762
|
if (await hasZitadelConfig(cwd)) return this.emit({
|
|
@@ -627,13 +772,26 @@ var Setup = class Setup extends BaseCommand {
|
|
|
627
772
|
framework = await orca.detect(cwd, flags.framework);
|
|
628
773
|
consola$1.success(`Detected ${framework.id}${framework.devPort ? ` (dev port ${framework.devPort})` : ""}`);
|
|
629
774
|
} catch (error) {
|
|
630
|
-
if (error instanceof ZitadelError && error.code === "E_FRAMEWORK_NOT_DETECTED"
|
|
775
|
+
if (error instanceof ZitadelError && error.code === "E_FRAMEWORK_NOT_DETECTED") {
|
|
776
|
+
const target = await inspectScaffoldTarget(cwd);
|
|
777
|
+
if (!target.scaffoldable) throw frameworkDetectionWithScaffoldTarget(error, cwd, target);
|
|
631
778
|
consola$1.info("Fresh app directory — scaffolding a fresh project");
|
|
632
779
|
framework = await orca.scaffold(cwd, await resolveScaffoldFramework(flags.framework, nonInteractive, orca));
|
|
633
780
|
scaffoldedFramework = true;
|
|
634
781
|
consola$1.success(`Scaffolded ${framework.id} skeleton`);
|
|
635
782
|
} else throw error;
|
|
636
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
|
+
});
|
|
637
795
|
if (flags["dev-port"] !== void 0) {
|
|
638
796
|
const devPort = flags["dev-port"];
|
|
639
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}`);
|
|
@@ -645,18 +803,30 @@ var Setup = class Setup extends BaseCommand {
|
|
|
645
803
|
}
|
|
646
804
|
let answers = {
|
|
647
805
|
server: this.meta.source,
|
|
648
|
-
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
|
|
649
810
|
};
|
|
650
811
|
if (!nonInteractive && !dryRun) {
|
|
651
812
|
intro("Zitadel setup");
|
|
652
813
|
const promptCtx = {
|
|
653
814
|
framework,
|
|
815
|
+
cwd,
|
|
654
816
|
serverFlag: this.meta.serverFlag,
|
|
655
|
-
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
|
|
656
821
|
};
|
|
657
822
|
for (const prompt of SETUP_PROMPTS) answers = await prompt.ask(answers, promptCtx);
|
|
658
823
|
outro("Configuration captured");
|
|
659
824
|
}
|
|
825
|
+
this.recordTelemetry({
|
|
826
|
+
preset: answers.preset,
|
|
827
|
+
use_case: answers.useCase,
|
|
828
|
+
design: answers.design ?? "built-in"
|
|
829
|
+
});
|
|
660
830
|
const issuer = issuerFromPort(answers.devPort);
|
|
661
831
|
framework = {
|
|
662
832
|
...framework,
|
|
@@ -665,8 +835,18 @@ var Setup = class Setup extends BaseCommand {
|
|
|
665
835
|
};
|
|
666
836
|
consola$1.start(`Creating project on ${answers.server}${dryRun ? " (dry run)" : ""}`);
|
|
667
837
|
const unauthClient = createZitadelClient({ baseUrl: answers.server });
|
|
668
|
-
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
|
+
});
|
|
669
847
|
consola$1.success(`Created project ${project.id}`);
|
|
848
|
+
this.recordTelemetry({ step: "project_created" });
|
|
849
|
+
const posture = derivePosture(framework.id, scaffoldedFramework);
|
|
670
850
|
const ctx = {
|
|
671
851
|
framework,
|
|
672
852
|
rendererId: flags.renderer ?? "react",
|
|
@@ -674,7 +854,10 @@ var Setup = class Setup extends BaseCommand {
|
|
|
674
854
|
issuer,
|
|
675
855
|
server: answers.server,
|
|
676
856
|
cliVersion: this.meta.cliVersion,
|
|
677
|
-
scaffoldedFramework
|
|
857
|
+
scaffoldedFramework,
|
|
858
|
+
posture,
|
|
859
|
+
preset: answers.preset,
|
|
860
|
+
useCase: answers.useCase
|
|
678
861
|
};
|
|
679
862
|
consola$1.start(`Patching project files${dryRun ? " (dry run)" : ""}`);
|
|
680
863
|
const result = await orca.patcherFor(framework.id).patch(ctx, {
|
|
@@ -683,12 +866,62 @@ var Setup = class Setup extends BaseCommand {
|
|
|
683
866
|
force
|
|
684
867
|
});
|
|
685
868
|
for (const file of result.filesWritten) {
|
|
686
|
-
const sentence = describeWrittenFile(
|
|
869
|
+
const sentence = describeWrittenFile(relativeDisplayPath(cwd, file), dryRun);
|
|
870
|
+
if (sentence) consola$1.info(sentence);
|
|
871
|
+
}
|
|
872
|
+
for (const file of result.filesSkipped) consola$1.info(`Left ${relativeDisplayPath(cwd, file)} unchanged (already matches target)`);
|
|
873
|
+
let resourceResult;
|
|
874
|
+
try {
|
|
875
|
+
resourceResult = dryRun ? { filesWritten: [] } : await materializeSetupResources({
|
|
876
|
+
cwd,
|
|
877
|
+
client: createZitadelClient({
|
|
878
|
+
baseUrl: answers.server,
|
|
879
|
+
token: project.project_secret
|
|
880
|
+
}),
|
|
881
|
+
projectId: project.id,
|
|
882
|
+
force,
|
|
883
|
+
preset: answers.preset,
|
|
884
|
+
useCase: answers.useCase,
|
|
885
|
+
design: answers.design,
|
|
886
|
+
cliVersion: this.meta.cliVersion
|
|
887
|
+
});
|
|
888
|
+
} catch (error) {
|
|
889
|
+
await rm(join(cwd, "zitadel.json"), { force: true });
|
|
890
|
+
await rm(join(cwd, ".zitadel/secret"), { force: true });
|
|
891
|
+
const cause = toZitadelError(error);
|
|
892
|
+
throw new ZitadelError(cause.code, `Default resource setup failed: ${cause.message}`, {
|
|
893
|
+
hint: "The project was created but its default schema/flow upload did not finish. Re-run `zitadel setup` to start over (add --force to overwrite partially written .zitadel files).",
|
|
894
|
+
nextCommands: ["zitadel setup --force"],
|
|
895
|
+
details: cause.details
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
for (const file of resourceResult.filesWritten) {
|
|
899
|
+
const sentence = describeWrittenFile(relativeDisplayPath(cwd, file), dryRun);
|
|
687
900
|
if (sentence) consola$1.info(sentence);
|
|
688
901
|
}
|
|
689
|
-
|
|
690
|
-
consola$1.success(`Patched ${
|
|
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
|
+
}
|
|
691
923
|
const installOutcome = await installDependenciesForSetup({
|
|
924
|
+
cliVersion: this.meta.cliVersion,
|
|
692
925
|
cwd,
|
|
693
926
|
depsAdded: result.depsAdded,
|
|
694
927
|
dryRun,
|
|
@@ -698,7 +931,22 @@ var Setup = class Setup extends BaseCommand {
|
|
|
698
931
|
scaffoldedFramework,
|
|
699
932
|
skipInstall: Boolean(flags["skip-install"])
|
|
700
933
|
});
|
|
701
|
-
|
|
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);
|
|
702
950
|
if (!this.jsonEnabled()) {
|
|
703
951
|
const sections = buildSummary({
|
|
704
952
|
projectFacts: await detectProjectFacts(cwd, framework.id),
|
|
@@ -706,14 +954,15 @@ var Setup = class Setup extends BaseCommand {
|
|
|
706
954
|
project,
|
|
707
955
|
server: answers.server,
|
|
708
956
|
issuer,
|
|
709
|
-
scaffoldedFramework
|
|
957
|
+
scaffoldedFramework,
|
|
958
|
+
design: answers.design
|
|
710
959
|
});
|
|
711
960
|
consola$1.box({
|
|
712
961
|
title: "Zitadel is ready",
|
|
713
962
|
message: [
|
|
714
963
|
renderSummary(sections),
|
|
715
964
|
"",
|
|
716
|
-
installOutcome.
|
|
965
|
+
[...installOutcome.boxActions, ...claimNudge.actions].join("\n")
|
|
717
966
|
].join("\n"),
|
|
718
967
|
style: {
|
|
719
968
|
padding: 1,
|
|
@@ -721,9 +970,11 @@ var Setup = class Setup extends BaseCommand {
|
|
|
721
970
|
borderColor: "green"
|
|
722
971
|
}
|
|
723
972
|
});
|
|
973
|
+
for (const warning of warnings) consola$1.warn(warning);
|
|
724
974
|
}
|
|
725
975
|
return this.emit({
|
|
726
976
|
status: "ok",
|
|
977
|
+
warnings,
|
|
727
978
|
pretty: "",
|
|
728
979
|
data: {
|
|
729
980
|
title: "Zitadel is ready.",
|
|
@@ -733,15 +984,35 @@ var Setup = class Setup extends BaseCommand {
|
|
|
733
984
|
},
|
|
734
985
|
framework: framework.id,
|
|
735
986
|
server: answers.server,
|
|
736
|
-
files_written:
|
|
737
|
-
|
|
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)),
|
|
738
994
|
install: installOutcome.install,
|
|
739
|
-
|
|
740
|
-
|
|
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]
|
|
741
1002
|
}
|
|
742
1003
|
});
|
|
743
1004
|
}
|
|
744
1005
|
};
|
|
1006
|
+
function frameworkDetectionWithScaffoldTarget(error, cwd, target) {
|
|
1007
|
+
return new ZitadelError(error.code, "Could not detect a supported app framework, and this directory is not a fresh scaffold target", {
|
|
1008
|
+
hint: `${target.reason ?? "Directory is not empty."} Run setup from an empty directory to scaffold a new app, or run setup from an existing supported app project.`,
|
|
1009
|
+
details: {
|
|
1010
|
+
cwd,
|
|
1011
|
+
entries: target.entries,
|
|
1012
|
+
reason: target.reason
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
745
1016
|
/**
|
|
746
1017
|
* Resolves which framework to scaffold into an empty directory: the explicit
|
|
747
1018
|
* `--framework`, else PickFrameworkPrompt, else a hard error in non-interactive
|
|
@@ -756,20 +1027,59 @@ async function resolveScaffoldFramework(framework, nonInteractive, orca) {
|
|
|
756
1027
|
function dryRunProject(issuer) {
|
|
757
1028
|
return {
|
|
758
1029
|
id: "dry-run-0000",
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
1030
|
+
project_secret: "sk_proj_dry_run_full",
|
|
1031
|
+
preview_secret: "sk_proj_dry_run_preview",
|
|
1032
|
+
preview_origins: [issuer],
|
|
1033
|
+
created_at: "2026-04-21T14:03:11.000Z"
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Reconstructs the flag list of the current invocation for retry guidance,
|
|
1038
|
+
* ending in `--server local`. Flags whose resolved value equals the default
|
|
1039
|
+
* are omitted — the retry reproduces the same outcome without them.
|
|
1040
|
+
*/
|
|
1041
|
+
function setupRetryFlags(opts) {
|
|
1042
|
+
const parts = [];
|
|
1043
|
+
if (opts.framework) parts.push(`--framework ${opts.framework}`);
|
|
1044
|
+
if (opts.preset && opts.preset !== DEFAULT_SETUP_PRESET) parts.push(`--preset ${opts.preset}`);
|
|
1045
|
+
if (opts.useCase && opts.useCase !== DEFAULT_SETUP_USE_CASE) parts.push(`--use-case ${opts.useCase}`);
|
|
1046
|
+
if (opts.design) parts.push(`--design ${opts.design}`);
|
|
1047
|
+
if (opts.renderer && opts.renderer !== "react") parts.push(`--renderer ${opts.renderer}`);
|
|
1048
|
+
if (opts.devPort !== void 0) parts.push(`--dev-port ${opts.devPort}`);
|
|
1049
|
+
if (opts.nonInteractive) parts.push("--non-interactive");
|
|
1050
|
+
parts.push("--server local");
|
|
1051
|
+
return parts.join(" ");
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Retry options straight from parsed flags, for failures before the wizard
|
|
1055
|
+
* resolves answers. `--non-interactive` is echoed only when explicitly passed
|
|
1056
|
+
* — TTY/JSON-inferred non-interactivity re-infers itself on the retry.
|
|
1057
|
+
*/
|
|
1058
|
+
function retryOptionsFromFlags(flags) {
|
|
1059
|
+
return {
|
|
1060
|
+
framework: flags.framework,
|
|
1061
|
+
preset: flags.preset,
|
|
1062
|
+
useCase: flags["use-case"],
|
|
1063
|
+
design: flags.design,
|
|
1064
|
+
renderer: flags.renderer,
|
|
1065
|
+
devPort: flags["dev-port"],
|
|
1066
|
+
nonInteractive: Boolean(flags["non-interactive"])
|
|
763
1067
|
};
|
|
764
1068
|
}
|
|
765
|
-
async function createProjectWithLocalHint(client, server, cliVersion, issuer,
|
|
1069
|
+
async function createProjectWithLocalHint(client, server, cliVersion, projectName, issuer, retry) {
|
|
766
1070
|
try {
|
|
767
|
-
|
|
1071
|
+
const payload = {
|
|
1072
|
+
name: projectName,
|
|
1073
|
+
preview_origins: [issuer],
|
|
1074
|
+
seed_defaults: false
|
|
1075
|
+
};
|
|
1076
|
+
return await client.createProject(payload);
|
|
768
1077
|
} catch (error) {
|
|
769
1078
|
const normalized = toZitadelError(error);
|
|
1079
|
+
const retryFlags = setupRetryFlags(retry);
|
|
770
1080
|
throw new ZitadelError(normalized.code, normalized.message, {
|
|
771
|
-
hint: `${normalized.hint ? `${normalized.hint} ` : ""}If you meant to use a local Zitadel server, start it first and retry setup with
|
|
772
|
-
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)],
|
|
773
1083
|
details: {
|
|
774
1084
|
server,
|
|
775
1085
|
original: normalized.details
|
|
@@ -777,20 +1087,20 @@ async function createProjectWithLocalHint(client, server, cliVersion, issuer, fr
|
|
|
777
1087
|
});
|
|
778
1088
|
}
|
|
779
1089
|
}
|
|
780
|
-
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) {
|
|
781
1095
|
const normalized = toZitadelError(error);
|
|
782
1096
|
if (normalized.code !== "E_LOCAL_SERVER_NOT_RUNNING") return error;
|
|
783
|
-
const setupCommand =
|
|
1097
|
+
const setupCommand = `setup ${setupRetryFlags(retry)}`;
|
|
784
1098
|
return new ZitadelError(normalized.code, normalized.message, {
|
|
785
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.`,
|
|
786
1100
|
nextCommands: [publicCliCommand("start", cliVersion), publicCliCommand(setupCommand, cliVersion)],
|
|
787
1101
|
details: normalized.details
|
|
788
1102
|
});
|
|
789
1103
|
}
|
|
790
|
-
/** Renders an absolute path relative to `cwd` for human-readable output. */
|
|
791
|
-
function relativeDisplay(cwd, path) {
|
|
792
|
-
return path.startsWith(cwd) ? path.slice(cwd.length + 1) : path;
|
|
793
|
-
}
|
|
794
1104
|
/**
|
|
795
1105
|
* Replaces the user's `$HOME` with `~` in a path for compact terminal output.
|
|
796
1106
|
* Falls back to the raw path when `HOME` isn't set or doesn't match.
|
|
@@ -811,15 +1121,13 @@ function pickWrittenFile(written, suffix) {
|
|
|
811
1121
|
}
|
|
812
1122
|
/**
|
|
813
1123
|
* Translates a patcher-written path into a single sentence the user can
|
|
814
|
-
* read at narration speed.
|
|
815
|
-
*
|
|
816
|
-
*
|
|
817
|
-
*
|
|
818
|
-
*
|
|
819
|
-
* 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.
|
|
820
1129
|
*/
|
|
821
1130
|
function describeWrittenFile(relPath, dryRun) {
|
|
822
|
-
if (relPath === ".zitadel" || relPath === ".zitadel/flows" || relPath === ".zitadel/schemas") return null;
|
|
823
1131
|
const verb = dryRun ? "Would write" : "Wrote";
|
|
824
1132
|
const sentence = SENTENCE_BY_PATH[relPath];
|
|
825
1133
|
if (sentence) return `${verb} ${sentence.subject} (${path(relPath)})`;
|
|
@@ -837,8 +1145,21 @@ const SENTENCE_BY_PATH = {
|
|
|
837
1145
|
"zitadel.json": { subject: "the Zitadel project configuration" },
|
|
838
1146
|
".env.example": { subject: "the .env example template" },
|
|
839
1147
|
".env.local": { subject: "the local development environment variables" },
|
|
840
|
-
".zitadel/state.json": { subject: "the
|
|
841
|
-
"
|
|
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" },
|
|
842
1163
|
"app/login/page.tsx": { subject: "the login page" },
|
|
843
1164
|
"app/register/page.tsx": { subject: "the registration page" },
|
|
844
1165
|
"app/profile/page.tsx": { subject: "the profile page" },
|
|
@@ -849,7 +1170,7 @@ const SENTENCE_BY_PATH = {
|
|
|
849
1170
|
};
|
|
850
1171
|
/** Builds the section list driving {@link renderSummary} for the setup command. */
|
|
851
1172
|
function buildSummary(opts) {
|
|
852
|
-
const { projectFacts, writtenRel, project, server, issuer, scaffoldedFramework } = opts;
|
|
1173
|
+
const { projectFacts, writtenRel, project, server, issuer, scaffoldedFramework, design } = opts;
|
|
853
1174
|
const sdkPackage = "@zitadel/sdk-next";
|
|
854
1175
|
const packageJsonHit = pickWrittenFile(writtenRel, "package.json");
|
|
855
1176
|
const detected = [{
|
|
@@ -867,7 +1188,7 @@ function buildSummary(opts) {
|
|
|
867
1188
|
secondary: path(fileNameOf(packageJsonHit))
|
|
868
1189
|
});
|
|
869
1190
|
for (const [label, suffix] of [
|
|
870
|
-
["Home
|
|
1191
|
+
["Home redirect", "app/page.tsx"],
|
|
871
1192
|
["Login page", "app/login/page.tsx"],
|
|
872
1193
|
["Register page", "app/register/page.tsx"],
|
|
873
1194
|
["Profile page", "app/profile/page.tsx"],
|
|
@@ -881,6 +1202,28 @@ function buildSummary(opts) {
|
|
|
881
1202
|
value: path(hit)
|
|
882
1203
|
});
|
|
883
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
|
+
});
|
|
884
1227
|
const projectRows = [
|
|
885
1228
|
{
|
|
886
1229
|
label: "Project id",
|
|
@@ -893,6 +1236,14 @@ function buildSummary(opts) {
|
|
|
893
1236
|
{
|
|
894
1237
|
label: "App will run",
|
|
895
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")
|
|
896
1247
|
}
|
|
897
1248
|
];
|
|
898
1249
|
return [
|
|
@@ -904,6 +1255,10 @@ function buildSummary(opts) {
|
|
|
904
1255
|
title: "Installed",
|
|
905
1256
|
rows: installedRows
|
|
906
1257
|
},
|
|
1258
|
+
{
|
|
1259
|
+
title: "Customize",
|
|
1260
|
+
rows: customizeRows
|
|
1261
|
+
},
|
|
907
1262
|
{
|
|
908
1263
|
title: "Project",
|
|
909
1264
|
rows: projectRows
|