@zitadel/cli 0.0.0

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.
@@ -0,0 +1,588 @@
1
+ import { Command, Flags } from "@oclif/core";
2
+ import consola from "consola";
3
+ import { readFile, stat } from "node:fs/promises";
4
+ import { join, resolve } from "node:path";
5
+ import { ApiError } from "@zitadel/api/runtime/fetch";
6
+ import { stringify } from "safe-stable-stringify";
7
+ //#region src/lib/errors.ts
8
+ /**
9
+ * Maps each {@link ZitadelErrorCode} to the process exit code the CLI
10
+ * returns. The table is the single source of truth for exit semantics so
11
+ * scripts and CI can branch on stable, documented numbers.
12
+ */
13
+ const EXIT_CODES = {
14
+ E_ALREADY_INIT: 0,
15
+ E_FRAMEWORK_NOT_DETECTED: 3,
16
+ E_UNSUPPORTED_PROJECT_SHAPE: 3,
17
+ E_NETWORK: 4,
18
+ E_AUTH: 1,
19
+ E_CONFLICT: 5,
20
+ E_VALIDATION: 3,
21
+ E_NOT_IMPLEMENTED: 2
22
+ };
23
+ /**
24
+ * The CLI's single error type. Carries a {@link ZitadelErrorCode} so the
25
+ * top-level handler can derive an exit code and structured output without
26
+ * pattern-matching on messages. Throwing this anywhere guarantees the user
27
+ * gets a categorised, hint-bearing failure instead of a raw stack trace.
28
+ */
29
+ var ZitadelError = class extends Error {
30
+ code;
31
+ hint;
32
+ nextCommands;
33
+ details;
34
+ constructor(code, message, opts = {}) {
35
+ super(message);
36
+ this.name = "ZitadelError";
37
+ this.code = code;
38
+ this.hint = opts.hint;
39
+ this.nextCommands = opts.nextCommands;
40
+ this.details = opts.details;
41
+ }
42
+ get exitCode() {
43
+ return EXIT_CODES[this.code] ?? 1;
44
+ }
45
+ };
46
+ /**
47
+ * Normalises any thrown value into a {@link ZitadelError}. Inspection is
48
+ * ordered most-specific-first (already-normalised, then errno/filesystem,
49
+ * network, Zod-like, generic `Error`, then a catch-all) so the most
50
+ * actionable category and hint win. This is the boundary that lets the rest
51
+ * of the CLI `throw` plain errors yet still produce consistent, categorised
52
+ * output. The original error shape is preserved under `details` for
53
+ * debugging without leaking it into the user-facing message.
54
+ */
55
+ function toZitadelError(error) {
56
+ if (error instanceof ZitadelError) return error;
57
+ if (error instanceof ApiError) return new ZitadelError(error.status === 401 || error.status === 403 ? "E_AUTH" : error.status >= 500 ? "E_NETWORK" : "E_VALIDATION", error.message, { details: {
58
+ status: error.status,
59
+ url: error.url,
60
+ body: error.body
61
+ } });
62
+ if (isErrnoException(error)) {
63
+ const details = { original: pickErrorShape(error) };
64
+ if (error.code === "EACCES" || error.code === "EPERM") return new ZitadelError("E_AUTH", `Permission denied: ${error.message}`, {
65
+ hint: "Check file permissions or run with the right user.",
66
+ details
67
+ });
68
+ if (error.code === "EEXIST") return new ZitadelError("E_CONFLICT", error.message, {
69
+ hint: "A file already exists. Use --force to overwrite or remove it first.",
70
+ details
71
+ });
72
+ if (error.code === "ENOENT") return new ZitadelError("E_VALIDATION", error.message, {
73
+ hint: "A required file or directory is missing.",
74
+ details
75
+ });
76
+ }
77
+ if (isNetworkError(error)) return new ZitadelError("E_NETWORK", errorMessage(error), {
78
+ hint: "Check your connection, ZITADEL_API_BASE, or the configured server URL.",
79
+ details: { original: pickErrorShape(error) }
80
+ });
81
+ if (isZodLikeError(error)) return new ZitadelError("E_VALIDATION", errorMessage(error), { details: { issues: error.issues } });
82
+ if (error instanceof Error) return new ZitadelError("E_VALIDATION", error.message, { details: { original: pickErrorShape(error) } });
83
+ return new ZitadelError("E_VALIDATION", "Unknown error", { details: error });
84
+ }
85
+ function isErrnoException(error) {
86
+ return error instanceof Error && typeof error.code === "string";
87
+ }
88
+ function isNetworkError(error) {
89
+ if (!(error instanceof Error)) return false;
90
+ if (error.name === "TypeError" && /fetch failed|network|ECONNREFUSED|ENOTFOUND/i.test(error.message)) return true;
91
+ const cause = error.cause;
92
+ if (cause && typeof cause === "object" && "code" in cause) {
93
+ const code = String(cause.code);
94
+ return /^(ECONNREFUSED|ECONNRESET|ENOTFOUND|ETIMEDOUT|EAI_AGAIN|UND_ERR)/i.test(code);
95
+ }
96
+ return false;
97
+ }
98
+ function isZodLikeError(error) {
99
+ return typeof error === "object" && error !== null && "issues" in error && Array.isArray(error.issues);
100
+ }
101
+ function errorMessage(error) {
102
+ if (error instanceof Error) return error.message;
103
+ if (typeof error === "string") return error;
104
+ return String(error);
105
+ }
106
+ function pickErrorShape(error) {
107
+ return {
108
+ name: error.name,
109
+ message: error.message,
110
+ code: error.code
111
+ };
112
+ }
113
+ //#endregion
114
+ //#region src/lib/json.ts
115
+ /**
116
+ * Serialise a value to pretty-printed JSON with object keys sorted at every
117
+ * depth. Determinism is the point: managed files written by the CLI must be
118
+ * byte-stable across runs so diffs stay clean and content hashes don't churn
119
+ * when only key ordering would otherwise differ. Delegates the deterministic
120
+ * sort to `safe-stable-stringify`, matching `JSON.stringify(value, null, 2)`
121
+ * formatting. The `?? "null"` only applies to `undefined`/function inputs,
122
+ * which the CLI never serialises.
123
+ */
124
+ function stableStringify(value) {
125
+ return stringify(value, null, 2) ?? "null";
126
+ }
127
+ /**
128
+ * Parse `contents` as JSON and assert the root is a plain object (not an
129
+ * array or scalar). The CLI's config and secret files are always objects, so
130
+ * this guards callers from the `JSON.parse` return type of `any` and produces
131
+ * a `path`-qualified error message pointing at the offending file.
132
+ */
133
+ function parseJsonObject(contents, path) {
134
+ const value = JSON.parse(contents);
135
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${path} must contain a JSON object`);
136
+ return value;
137
+ }
138
+ /**
139
+ * Narrows an unknown value to a plain (non-array, non-null) object. Shared by
140
+ * the commands and the file-writer that walk parsed JSON, so the predicate
141
+ * isn't reimplemented per call site.
142
+ */
143
+ function isObject(value) {
144
+ return typeof value === "object" && value !== null && !Array.isArray(value);
145
+ }
146
+ //#endregion
147
+ //#region src/lib/server.ts
148
+ /**
149
+ * Server URL used when nothing else resolves. Also surfaced in hints and
150
+ * the interactive setup prompt as the suggested value, so it is exported
151
+ * rather than kept private.
152
+ */
153
+ const DEFAULT_SERVER = "https://api.zitadel.cloud";
154
+ /**
155
+ * Resolves which server the CLI should target, applying a fixed
156
+ * precedence: explicit `--server` flag, then `ZITADEL_API_BASE`, then the
157
+ * selected environment block in `zitadel.json`, then the config's
158
+ * top-level `server`, falling back to {@link DEFAULT_SERVER}. Every
159
+ * candidate is validated to a normalised origin; an invalid URL throws a
160
+ * `ZitadelError` rather than silently falling through.
161
+ */
162
+ async function resolveServer(input) {
163
+ if (input.serverFlag) return validate({
164
+ value: input.serverFlag,
165
+ origin: "flag"
166
+ });
167
+ const envValue = input.env.ZITADEL_API_BASE;
168
+ if (envValue) return validate({
169
+ value: envValue,
170
+ origin: "env"
171
+ });
172
+ const config = await readConfig(input.cwd);
173
+ if (config) {
174
+ const envBranch = readEnvServer(config, input.environment);
175
+ if (envBranch) return validate({
176
+ value: envBranch,
177
+ origin: "config-env"
178
+ });
179
+ if (typeof config.server === "string") return validate({
180
+ value: config.server,
181
+ origin: "config-top"
182
+ });
183
+ }
184
+ return {
185
+ value: DEFAULT_SERVER,
186
+ origin: "default"
187
+ };
188
+ }
189
+ function validate(resolved) {
190
+ try {
191
+ const url = new URL(resolved.value);
192
+ if (url.protocol !== "https:" && url.protocol !== "http:") throw new ZitadelError("E_VALIDATION", `Server URL must use http(s): ${resolved.value}`, { hint: `Set "server" in zitadel.json to a URL like ${DEFAULT_SERVER}.` });
193
+ return {
194
+ value: url.origin,
195
+ origin: resolved.origin
196
+ };
197
+ } catch (error) {
198
+ if (error instanceof ZitadelError) throw error;
199
+ throw new ZitadelError("E_VALIDATION", `Invalid server "${resolved.value}"`, {
200
+ hint: `Use a URL like ${DEFAULT_SERVER}.`,
201
+ details: { origin: resolved.origin }
202
+ });
203
+ }
204
+ }
205
+ async function readConfig(cwd) {
206
+ try {
207
+ return parseJsonObject(await readFile(join(cwd, "zitadel.json"), "utf8"), "zitadel.json");
208
+ } catch (error) {
209
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return;
210
+ throw error;
211
+ }
212
+ }
213
+ function readEnvServer(config, environment) {
214
+ if (!environment) return;
215
+ const envs = config.environments;
216
+ if (!isObject(envs)) return;
217
+ const branch = envs[environment];
218
+ if (!isObject(branch)) return;
219
+ return typeof branch.server === "string" ? branch.server : void 0;
220
+ }
221
+ //#endregion
222
+ //#region src/lib/paths.ts
223
+ /**
224
+ * Resolve the working directory the CLI should operate against, defaulting to
225
+ * the process CWD when no `--cwd` override is given. Always returns an
226
+ * absolute path so downstream `join`/`readFile` calls are unaffected by later
227
+ * `process.chdir` or relative-path ambiguity.
228
+ */
229
+ function resolveCwd(cwd) {
230
+ return resolve(cwd ?? process.cwd());
231
+ }
232
+ /**
233
+ * Sentinel comment stamped at the top of every file the CLI generates and
234
+ * owns. Commands like `doctor` and `eject` look for this marker to decide
235
+ * whether a file is safe to touch; the trailing `v1` lets the format evolve
236
+ * without mistaking newer managed files for hand-edited ones.
237
+ */
238
+ const MANAGED_MARKER = "// zitadel-cli: managed-file v1";
239
+ //#endregion
240
+ //#region src/lib/oclif/base.ts
241
+ /**
242
+ * Base class for every oclif command. Owns the global flags, builds the
243
+ * {@link GlobalOptions} context (including server `source` resolution) the
244
+ * subclass's `run` reads via `this.meta`, and turns the {@link CommandResult}
245
+ * it returns into the JSON envelope (oclif serialises it natively in `--json`
246
+ * mode) or human-facing text. Errors are translated into the failure envelope
247
+ * and the mapped process exit code. Subclasses stay thin: parse flags, call
248
+ * {@link toMeta}, do their work, and `return this.emit(...)`. The agent
249
+ * contract (ADR 004) is preserved — oclif only replaces parsing, dispatch,
250
+ * help, and JSON emission.
251
+ */
252
+ var BaseCommand = class extends Command {
253
+ /** Opt into oclif's native `--json` flag and JSON serialisation of the result. */
254
+ static enableJsonFlag = true;
255
+ /** Flags shared by every command, inherited via oclif `baseFlags`. */
256
+ static baseFlags = {
257
+ cwd: Flags.string({
258
+ char: "c",
259
+ description: "Project directory to operate on."
260
+ }),
261
+ server: Flags.string({
262
+ char: "s",
263
+ description: "Override the resolved server URL."
264
+ }),
265
+ "non-interactive": Flags.boolean({
266
+ char: "n",
267
+ description: "Disable prompts. Required when scripting or running as an agent."
268
+ }),
269
+ force: Flags.boolean({
270
+ char: "f",
271
+ description: "Overwrite protected files on conflict."
272
+ }),
273
+ "dry-run": Flags.boolean({ description: "Preview without mutating files or the platform." }),
274
+ verbose: Flags.boolean({ description: "Verbose logging." }),
275
+ debug: Flags.boolean({ description: "Debug logging." })
276
+ };
277
+ /** Resolved context for the current invocation; set by {@link toMeta}. */
278
+ meta = this.fallbackMeta();
279
+ /**
280
+ * Builds {@link GlobalOptions} from parsed flags, resolving the server
281
+ * `source` by the documented precedence and storing the result on
282
+ * `this.meta` so the error handler can render a complete envelope.
283
+ */
284
+ async toMeta(flags) {
285
+ const cwd = resolveCwd(typeof flags.cwd === "string" ? flags.cwd : void 0);
286
+ const serverFlag = typeof flags.server === "string" ? flags.server : void 0;
287
+ const environment = typeof flags.environment === "string" ? flags.environment : "development";
288
+ const source = await resolveServer({
289
+ cwd,
290
+ env: process.env,
291
+ serverFlag,
292
+ environment
293
+ });
294
+ const json = this.jsonEnabled();
295
+ const isTTY = Boolean(process.stdout.isTTY && process.stdin.isTTY);
296
+ const verbose = Boolean(flags.verbose);
297
+ const debug = Boolean(flags.debug);
298
+ consola.level = json ? -999 : debug ? 4 : 3;
299
+ consola.options.formatOptions = {
300
+ ...consola.options.formatOptions,
301
+ date: false,
302
+ colors: true,
303
+ compact: true
304
+ };
305
+ this.meta = {
306
+ cwd,
307
+ nonInteractive: Boolean(flags["non-interactive"]) || !isTTY || json,
308
+ dryRun: Boolean(flags["dry-run"]),
309
+ force: Boolean(flags.force),
310
+ command: this.id ?? "(default)",
311
+ cliVersion: this.config.version,
312
+ source: source.value,
313
+ serverFlag,
314
+ verbose,
315
+ debug,
316
+ env: process.env,
317
+ isTTY
318
+ };
319
+ return this.meta;
320
+ }
321
+ /**
322
+ * Final step of every command: in human mode it prints the rendered result
323
+ * (oclif suppresses {@link Command.log} under `--json`); it returns the
324
+ * envelope so oclif's `--json` path serialises it.
325
+ */
326
+ emit(result) {
327
+ this.log(renderPretty(result, this.meta));
328
+ return toEnvelope(result, this.meta);
329
+ }
330
+ /**
331
+ * Renders any thrown error as the failure envelope and exits with its code.
332
+ * A flag-parse error fires before {@link toMeta} runs, so the local `meta`
333
+ * here refreshes `command` from the now-resolved command id to keep the
334
+ * envelope's `command` field accurate.
335
+ */
336
+ async catch(error) {
337
+ const meta = {
338
+ ...this.meta,
339
+ command: this.id ?? this.meta.command
340
+ };
341
+ const zitadelError = toZitadelError(error);
342
+ if (this.jsonEnabled()) this.logJson(toErrorEnvelope(zitadelError, meta));
343
+ else this.logToStderr(renderError(zitadelError));
344
+ return this.exit(zitadelError.exitCode);
345
+ }
346
+ /**
347
+ * Context used before {@link toMeta} runs, so an error thrown during flag
348
+ * parsing still renders a complete envelope. Version comes from oclif's
349
+ * resolved {@link Command.config}.
350
+ */
351
+ fallbackMeta() {
352
+ return {
353
+ cwd: resolveCwd(void 0),
354
+ nonInteractive: false,
355
+ dryRun: false,
356
+ force: false,
357
+ command: "(default)",
358
+ cliVersion: this.config.version,
359
+ source: "",
360
+ verbose: false,
361
+ debug: false,
362
+ env: process.env,
363
+ isTTY: Boolean(process.stdout.isTTY && process.stdin.isTTY)
364
+ };
365
+ }
366
+ };
367
+ /** Wraps a {@link CommandResult} with the invocation metadata into the final envelope. */
368
+ function toEnvelope(result, meta) {
369
+ const base = {
370
+ cli_version: meta.cliVersion,
371
+ command: meta.command,
372
+ source: meta.source
373
+ };
374
+ if (result.status === "ok") return {
375
+ ...base,
376
+ status: "ok",
377
+ data: result.data,
378
+ warnings: result.warnings ? [...result.warnings] : []
379
+ };
380
+ return {
381
+ ...base,
382
+ status: "skipped",
383
+ reason: result.reason,
384
+ data: result.data,
385
+ next_commands: result.nextCommands ? [...result.nextCommands] : void 0
386
+ };
387
+ }
388
+ /** Builds the failure envelope from a {@link ZitadelError} and the invocation metadata. */
389
+ function toErrorEnvelope(error, meta) {
390
+ return {
391
+ status: "error",
392
+ cli_version: meta.cliVersion,
393
+ command: meta.command,
394
+ source: meta.source,
395
+ code: error.code,
396
+ message: error.message,
397
+ hint: error.hint,
398
+ next_commands: error.nextCommands,
399
+ details: error.details
400
+ };
401
+ }
402
+ /**
403
+ * Renders a {@link CommandResult} as human-facing text for non-JSON mode. A
404
+ * command may supply a bespoke `pretty` string (e.g. the `apply` plan diff);
405
+ * otherwise success payloads are summarised by {@link formatData} and skips are
406
+ * shown with their reason and follow-up commands.
407
+ */
408
+ function renderPretty(result, meta) {
409
+ if (result.pretty !== void 0) return result.pretty;
410
+ if (result.status === "ok") return formatData(result.data, result.warnings ? [...result.warnings] : [], meta);
411
+ const lines = [`Skipped: ${result.reason}${suffixBlock(meta)}`];
412
+ if (result.nextCommands && result.nextCommands.length > 0) {
413
+ lines.push("Next:");
414
+ for (const cmd of result.nextCommands) lines.push(` $ ${cmd}`);
415
+ }
416
+ return lines.join("\n");
417
+ }
418
+ /**
419
+ * Renders a {@link ZitadelError} as a human-readable block for stderr: the
420
+ * coded message, an optional hint, and any suggested next commands.
421
+ */
422
+ function renderError(error) {
423
+ const lines = [`Error ${error.code}: ${error.message}`];
424
+ if (error.hint) lines.push(error.hint);
425
+ if (error.nextCommands && error.nextCommands.length > 0) {
426
+ lines.push("Next:");
427
+ for (const cmd of error.nextCommands) lines.push(` $ ${cmd}`);
428
+ }
429
+ return lines.join("\n");
430
+ }
431
+ function formatData(data, warnings, opts) {
432
+ if (typeof data === "string") {
433
+ const suffix = sourceSuffix(opts);
434
+ return suffix ? `${data}\n${suffix}` : data;
435
+ }
436
+ const lines = [];
437
+ const titleLine = isObject(data) && typeof data.title === "string" ? String(data.title) : "Zitadel command completed.";
438
+ lines.push(titleLine);
439
+ const suffix = sourceSuffix(opts);
440
+ if (suffix) lines.push(suffix);
441
+ if (isObject(data)) {
442
+ renderKnownSections(lines, data);
443
+ if (Array.isArray(data.next_actions) && data.next_actions.length > 0) {
444
+ lines.push("");
445
+ lines.push("Next:");
446
+ for (const action of data.next_actions) lines.push(` ${String(action)}`);
447
+ }
448
+ if (Array.isArray(data.next_commands) && data.next_commands.length > 0) {
449
+ if (!Array.isArray(data.next_actions) || data.next_actions.length === 0) {
450
+ lines.push("");
451
+ lines.push("Next:");
452
+ }
453
+ for (const cmd of data.next_commands) lines.push(` $ ${String(cmd)}`);
454
+ }
455
+ }
456
+ for (const warning of warnings) lines.push(`Warning: ${warning}`);
457
+ return lines.join("\n");
458
+ }
459
+ function renderKnownSections(lines, data) {
460
+ if (isObject(data.project)) {
461
+ const project = data.project;
462
+ const segments = [];
463
+ if (typeof project.project_id === "string") segments.push(`project=${project.project_id}`);
464
+ if (typeof project.lifecycle === "string") segments.push(`lifecycle=${project.lifecycle}`);
465
+ if (typeof project.issuer === "string") segments.push(`issuer=${project.issuer}`);
466
+ if (segments.length > 0) lines.push(`Project: ${segments.join(" ")}`);
467
+ }
468
+ if (typeof data.framework === "string") lines.push(`framework=${data.framework}`);
469
+ if (Array.isArray(data.files_written) || Array.isArray(data.files_skipped)) {
470
+ const written = Array.isArray(data.files_written) ? data.files_written.length : 0;
471
+ const skippedCount = Array.isArray(data.files_skipped) ? data.files_skipped.length : 0;
472
+ lines.push(`Files: ${written} written, ${skippedCount} unchanged`);
473
+ }
474
+ if (isObject(data.apply)) {
475
+ const apply = data.apply;
476
+ const bits = [];
477
+ if (typeof apply.config_version === "number") bits.push(`v${apply.config_version}`);
478
+ if (typeof apply.hash === "string") bits.push(`hash=${String(apply.hash).slice(0, 12)}`);
479
+ if (typeof apply.environment === "string") bits.push(`env=${apply.environment}`);
480
+ if (bits.length > 0) lines.push(`Apply: ${bits.join(" ")}`);
481
+ }
482
+ if (Array.isArray(data.checks) && data.checks.length > 0) {
483
+ lines.push("Checks:");
484
+ for (const check of data.checks) {
485
+ if (!isObject(check)) continue;
486
+ const status = check.status === "pass" ? "ok" : "fail";
487
+ lines.push(` [${status}] ${String(check.name ?? "check")}: ${String(check.message ?? "")}`);
488
+ }
489
+ }
490
+ }
491
+ function sourceSuffix(opts) {
492
+ try {
493
+ const url = new URL(opts.source);
494
+ if (url.host === "api.zitadel.cloud") return "";
495
+ return `(server: ${url.host})`;
496
+ } catch {
497
+ return "";
498
+ }
499
+ }
500
+ function suffixBlock(opts) {
501
+ const suffix = sourceSuffix(opts);
502
+ return suffix ? ` ${suffix}` : "";
503
+ }
504
+ //#endregion
505
+ //#region src/lib/project.ts
506
+ /**
507
+ * Reports whether `cwd` has already been initialized, i.e. a committed
508
+ * `zitadel.json` exists. Used to decide whether setup should run or skip.
509
+ */
510
+ async function hasZitadelConfig(cwd) {
511
+ return exists(join(cwd, "zitadel.json"));
512
+ }
513
+ /**
514
+ * Reports whether local secret material (`.zitadel/secret`) is present. Gates
515
+ * commands that need credentials, and signals that secrets were already pulled.
516
+ */
517
+ async function hasZitadelSecret(cwd) {
518
+ return exists(join(cwd, ".zitadel/secret"));
519
+ }
520
+ async function exists(path) {
521
+ try {
522
+ await stat(path);
523
+ return true;
524
+ } catch (error) {
525
+ if (isNotFound(error)) return false;
526
+ throw error;
527
+ }
528
+ }
529
+ /**
530
+ * Reads and parses `zitadel.json` into a plain object. Translates a missing
531
+ * file into an actionable `E_VALIDATION` error pointing at `zitadel setup`;
532
+ * other errors (e.g. malformed JSON) propagate unchanged.
533
+ */
534
+ async function readZitadelConfig(cwd) {
535
+ try {
536
+ return parseJsonObject(await readFile(join(cwd, "zitadel.json"), "utf8"), "zitadel.json");
537
+ } catch (error) {
538
+ if (isNotFound(error)) throw new ZitadelError("E_VALIDATION", "zitadel.json was not found", {
539
+ hint: "Run `zitadel setup` first.",
540
+ nextCommands: ["zitadel setup"]
541
+ });
542
+ throw error;
543
+ }
544
+ }
545
+ /**
546
+ * Reads, parses, and structurally validates `.zitadel/secret`, returning it
547
+ * as a {@link ZitadelSecret}. A missing file becomes an actionable
548
+ * `E_VALIDATION` error pointing at `zitadel setup` / `zitadel doctor --fix`;
549
+ * a present-but-incomplete file throws so callers never proceed with partial
550
+ * credentials.
551
+ */
552
+ async function readZitadelSecret(cwd) {
553
+ try {
554
+ const secret = parseJsonObject(await readFile(join(cwd, ".zitadel/secret"), "utf8"), ".zitadel/secret");
555
+ if (typeof secret.project_id !== "string" || typeof secret.project_secret !== "string" || typeof secret.preview_secret !== "string" || !Array.isArray(secret.preview_origins)) throw new Error(".zitadel/secret is missing required fields");
556
+ return secret;
557
+ } catch (error) {
558
+ if (isNotFound(error)) throw new ZitadelError("E_VALIDATION", ".zitadel/secret was not found", {
559
+ hint: "Run `zitadel setup` first, or restore the project secret with `zitadel doctor --fix`.",
560
+ nextCommands: ["zitadel setup", "zitadel doctor --fix"]
561
+ });
562
+ throw error;
563
+ }
564
+ }
565
+ /**
566
+ * Reads the configured renderer id from a parsed `zitadel.json`, normalising the
567
+ * legacy `default` alias to `react` and falling back to `react` when unset. The
568
+ * value is validated downstream by `getRenderer`, so callers need not re-check.
569
+ */
570
+ function readRendererId(config) {
571
+ const branding = isObject(config.branding) ? config.branding : void 0;
572
+ const value = branding && typeof branding.renderer === "string" ? branding.renderer : "react";
573
+ return value === "default" ? "react" : value;
574
+ }
575
+ /** Reads `environments.development.issuer` from a parsed `zitadel.json`, if present. */
576
+ function readDevelopmentIssuer(config) {
577
+ if (isObject(config.environments) && isObject(config.environments.development)) {
578
+ const issuer = config.environments.development.issuer;
579
+ return typeof issuer === "string" ? issuer : void 0;
580
+ }
581
+ }
582
+ function isNotFound(error) {
583
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
584
+ }
585
+ //#endregion
586
+ export { readZitadelConfig as a, MANAGED_MARKER as c, parseJsonObject as d, stableStringify as f, readRendererId as i, DEFAULT_SERVER as l, hasZitadelSecret as n, readZitadelSecret as o, ZitadelError as p, readDevelopmentIssuer as r, BaseCommand as s, hasZitadelConfig as t, isObject as u };
587
+
588
+ //# sourceMappingURL=project-C3pSfbao.mjs.map