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