@zitadel/cli 0.1.0-alpha.8 → 1.0.0-alpha.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +250 -62
  2. package/SKILLS.md +227 -38
  3. package/dist/branding-cygSBPkV.mjs +79 -0
  4. package/dist/branding-cygSBPkV.mjs.map +1 -0
  5. package/dist/claim-state-DlfD6Res.mjs +66 -0
  6. package/dist/claim-state-DlfD6Res.mjs.map +1 -0
  7. package/dist/commands/apply.mjs +27 -8
  8. package/dist/commands/apply.mjs.map +1 -1
  9. package/dist/commands/branding/eject.mjs +104 -0
  10. package/dist/commands/branding/eject.mjs.map +1 -0
  11. package/dist/commands/claim.mjs +402 -0
  12. package/dist/commands/claim.mjs.map +1 -0
  13. package/dist/commands/doctor.mjs +687 -55
  14. package/dist/commands/doctor.mjs.map +1 -1
  15. package/dist/commands/eject.mjs +18 -6
  16. package/dist/commands/eject.mjs.map +1 -1
  17. package/dist/commands/logs.mjs +2 -2
  18. package/dist/commands/plan.mjs +19 -6
  19. package/dist/commands/plan.mjs.map +1 -1
  20. package/dist/commands/reset.mjs +12 -4
  21. package/dist/commands/reset.mjs.map +1 -1
  22. package/dist/commands/schemas/list.mjs +146 -0
  23. package/dist/commands/schemas/list.mjs.map +1 -0
  24. package/dist/commands/setup.mjs +642 -287
  25. package/dist/commands/setup.mjs.map +1 -1
  26. package/dist/commands/start.mjs +73 -6
  27. package/dist/commands/start.mjs.map +1 -1
  28. package/dist/commands/status.mjs +52 -13
  29. package/dist/commands/status.mjs.map +1 -1
  30. package/dist/commands/stop.mjs +59 -6
  31. package/dist/commands/stop.mjs.map +1 -1
  32. package/dist/designs-Ckz18Dpo.mjs +38 -0
  33. package/dist/designs-Ckz18Dpo.mjs.map +1 -0
  34. package/dist/{docker-BA78SdC2.mjs → docker-DcRGTOJa.mjs} +61 -11
  35. package/dist/docker-DcRGTOJa.mjs.map +1 -0
  36. package/dist/environment-rjRVkJjW.mjs +17 -0
  37. package/dist/environment-rjRVkJjW.mjs.map +1 -0
  38. package/dist/journey-guidance-BGrOX_gT.mjs +40 -0
  39. package/dist/journey-guidance-BGrOX_gT.mjs.map +1 -0
  40. package/dist/oclif-CanO3zdt.mjs +1769 -0
  41. package/dist/oclif-CanO3zdt.mjs.map +1 -0
  42. package/dist/{orca-CfKDQRop.mjs → orca-BzQIQqzt.mjs} +1663 -303
  43. package/dist/orca-BzQIQqzt.mjs.map +1 -0
  44. package/dist/package-manager-DFdLcDx1.mjs +194 -0
  45. package/dist/package-manager-DFdLcDx1.mjs.map +1 -0
  46. package/dist/ports-BM20XIZb.mjs +116 -0
  47. package/dist/ports-BM20XIZb.mjs.map +1 -0
  48. package/dist/processes-DKHPkU8O.mjs +120 -0
  49. package/dist/processes-DKHPkU8O.mjs.map +1 -0
  50. package/dist/{project-CKAHtHML.mjs → project-CHhot85s.mjs} +57 -4
  51. package/dist/project-CHhot85s.mjs.map +1 -0
  52. package/dist/sync-BBlACKzT.mjs +1633 -0
  53. package/dist/sync-BBlACKzT.mjs.map +1 -0
  54. package/dist/user-schema-DTuOsdKE.mjs +91 -0
  55. package/dist/user-schema-DTuOsdKE.mjs.map +1 -0
  56. package/oclif.manifest.json +412 -5
  57. package/package.json +10 -5
  58. package/dist/docker-BA78SdC2.mjs.map +0 -1
  59. package/dist/docker-guidance-BvfpmsDj.mjs +0 -21
  60. package/dist/docker-guidance-BvfpmsDj.mjs.map +0 -1
  61. package/dist/oclif-VkCTGIEk.mjs +0 -818
  62. package/dist/oclif-VkCTGIEk.mjs.map +0 -1
  63. package/dist/orca-CfKDQRop.mjs.map +0 -1
  64. package/dist/project-CKAHtHML.mjs.map +0 -1
  65. package/dist/sync-B5lqgQO3.mjs +0 -733
  66. package/dist/sync-B5lqgQO3.mjs.map +0 -1
@@ -0,0 +1,1769 @@
1
+ import { access, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { Command, Flags } from "@oclif/core";
4
+ import consola from "consola";
5
+ import { ApiError, apiErrorMessage } from "@zitadel/api/runtime/fetch";
6
+ import { createHash, randomUUID } from "node:crypto";
7
+ import { stringify } from "safe-stable-stringify";
8
+ import { constants, mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
10
+ import mixpanelLib from "mixpanel";
11
+ import { homedir } from "node:os";
12
+ //#region src/lib/oclif/server-kind.ts
13
+ /**
14
+ * Buckets the resolved backend `source` into a coarse kind. The raw URL is never
15
+ * emitted — it can carry an internal/self-hosted hostname — only which kind of
16
+ * backend the command targeted.
17
+ */
18
+ var ServerKind = class {
19
+ value(source) {
20
+ if (source === "mock") return "local";
21
+ if (!URL.canParse(source)) return "unknown";
22
+ const { hostname } = new URL(source);
23
+ if (hostname === "zitadel.cloud" || hostname.endsWith(".zitadel.cloud")) return "cloud";
24
+ if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1") return "local";
25
+ return "self_hosted";
26
+ }
27
+ };
28
+ const serverKind = new ServerKind();
29
+ //#endregion
30
+ //#region src/lib/public-cli.ts
31
+ const CLI_PACKAGE_NAME = "@zitadel/cli";
32
+ function npmDistTagForCliVersion(cliVersion) {
33
+ return cliVersion.trim().replace(/^v/, "").match(/^\d+\.\d+\.\d+-([0-9A-Za-z][0-9A-Za-z-]*)/)?.[1] ?? "latest";
34
+ }
35
+ function npmSelectorForCliVersion(cliVersion) {
36
+ const normalized = cliVersion.trim().replace(/^v/, "");
37
+ if (/^\d+\.\d+\.\d+-alpha\.\d+$/.test(normalized)) return normalized;
38
+ return npmDistTagForCliVersion(normalized);
39
+ }
40
+ function publicCliCommand(args, cliVersion) {
41
+ const prefix = `npx ${CLI_PACKAGE_NAME}@${npmSelectorForCliVersion(cliVersion)}`;
42
+ return args.length > 0 ? `${prefix} ${args}` : prefix;
43
+ }
44
+ function normalizePublicCliCommand(command, cliVersion) {
45
+ if (command === "zitadel") return publicCliCommand("", cliVersion);
46
+ if (command.startsWith("zitadel ")) return publicCliCommand(command.slice(8), cliVersion);
47
+ return command;
48
+ }
49
+ function normalizePublicCliCommands(commands, cliVersion) {
50
+ return commands?.map((command) => normalizePublicCliCommand(command, cliVersion));
51
+ }
52
+ /**
53
+ * Rewrites `zitadel …` command mentions in scaffolded prose (the
54
+ * `.zitadel/**` READMEs) to the public `npx @zitadel/cli@<version> …` form.
55
+ *
56
+ * The checked-in README sources stay written with the bare `zitadel`
57
+ * command — that is the canonical, readable spelling — but the CLI is not a
58
+ * dependency of the scaffolded app, so the bare command does not exist on
59
+ * the user's PATH (and `npx zitadel` would fetch an unrelated npm package).
60
+ * Covers inline code spans (`` `zitadel plan` ``) and fenced-block lines
61
+ * that start with `zitadel `; prose file names like `zitadel.json` and
62
+ * `zitadel.db` don't match because the word must end the span or be
63
+ * followed by a space.
64
+ */
65
+ function normalizePublicCliProse(content, cliVersion) {
66
+ const prefix = publicCliCommand("", cliVersion);
67
+ return rewriteCliCodeSpans(content, cliVersion).replace(/^(\s*)zitadel (.*)$/gm, (_match, indent, rest) => `${indent}${prefix} ${rest}`);
68
+ }
69
+ /** The inline-code-span half of {@link normalizePublicCliProse}. */
70
+ function rewriteCliCodeSpans(content, cliVersion) {
71
+ const prefix = publicCliCommand("", cliVersion);
72
+ return content.replace(/`zitadel( [^`\n]*)?`/g, (_match, args) => `\`${prefix}${args ?? ""}\``);
73
+ }
74
+ /**
75
+ * Rewrites `zitadel …` command mentions inside every string of a scaffolded
76
+ * JSON document — today the `.zitadel/meta/*.json` dialect files — returning
77
+ * a rewritten copy. The input is never mutated: the meta-schema bodies are
78
+ * imported JSON modules shared across calls.
79
+ *
80
+ * Those files are byte-copies of the meta-schemas the server embeds
81
+ * (`api/openapi/endpoints/schemas/*.json`), where the bare `zitadel` spelling
82
+ * is the correct one — the server documents the product command, not one
83
+ * project's install. Their `description` strings surface as editor tooltips
84
+ * on the scaffolded `.zitadel/**` files, where the bare command does not
85
+ * exist on the user's PATH, so the copy is normalized on the way out instead
86
+ * of the shared source being edited (same fix PR #872 made for the READMEs).
87
+ *
88
+ * Unlike {@link normalizePublicCliProse} this rewrites inline code spans
89
+ * only: a JSON `description` is prose, so a value that merely begins with
90
+ * the word `zitadel` is a sentence, not a command line.
91
+ */
92
+ function normalizePublicCliJson(value, cliVersion) {
93
+ if (typeof value === "string") return rewriteCliCodeSpans(value, cliVersion);
94
+ if (Array.isArray(value)) return value.map((item) => normalizePublicCliJson(item, cliVersion));
95
+ if (typeof value === "object" && value !== null) return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, normalizePublicCliJson(item, cliVersion)]));
96
+ return value;
97
+ }
98
+ //#endregion
99
+ //#region src/lib/errors.ts
100
+ /**
101
+ * Maps each {@link ZitadelErrorCode} to the process exit code the CLI
102
+ * returns. The table is the single source of truth for exit semantics so
103
+ * scripts and CI can branch on stable, documented numbers.
104
+ */
105
+ const EXIT_CODES = {
106
+ E_ALREADY_INIT: 0,
107
+ E_FRAMEWORK_NOT_DETECTED: 3,
108
+ E_UNSUPPORTED_PROJECT_SHAPE: 3,
109
+ E_NETWORK: 4,
110
+ E_AUTH: 1,
111
+ E_CONFLICT: 5,
112
+ E_LOCAL_SERVER_NOT_RUNNING: 4,
113
+ E_NOT_FOUND: 4,
114
+ E_PORT_IN_USE: 5,
115
+ E_VALIDATION: 3,
116
+ E_NOT_IMPLEMENTED: 2
117
+ };
118
+ /**
119
+ * The CLI's single error type. Carries a {@link ZitadelErrorCode} so the
120
+ * top-level handler can derive an exit code and structured output without
121
+ * pattern-matching on messages. Throwing this anywhere guarantees the user
122
+ * gets a categorised, hint-bearing failure instead of a raw stack trace.
123
+ */
124
+ var ZitadelError = class extends Error {
125
+ code;
126
+ hint;
127
+ nextCommands;
128
+ details;
129
+ constructor(code, message, opts = {}) {
130
+ super(message);
131
+ this.name = "ZitadelError";
132
+ this.code = code;
133
+ this.hint = opts.hint;
134
+ this.nextCommands = opts.nextCommands;
135
+ this.details = opts.details;
136
+ }
137
+ get exitCode() {
138
+ return EXIT_CODES[this.code] ?? 1;
139
+ }
140
+ };
141
+ /**
142
+ * Normalises any thrown value into a {@link ZitadelError}. Inspection is
143
+ * ordered most-specific-first (already-normalised, then errno/filesystem,
144
+ * network, Zod-like, generic `Error`, then a catch-all) so the most
145
+ * actionable category and hint win. This is the boundary that lets the rest
146
+ * of the CLI `throw` plain errors yet still produce consistent, categorised
147
+ * output. The original error shape is preserved under `details` for
148
+ * debugging without leaking it into the user-facing message.
149
+ */
150
+ function toZitadelError(error) {
151
+ if (error instanceof ZitadelError) return error;
152
+ if (error instanceof ApiError) {
153
+ const details = {
154
+ status: error.status,
155
+ url: error.url,
156
+ body: error.body
157
+ };
158
+ if (error.status === 404) return new ZitadelError("E_NOT_FOUND", !isPlatformErrorEnvelope(error.body) ? `${apiErrorMessage(error)} — ${error.url} has no such endpoint; is this a Zitadel platform API?` : apiErrorMessage(error), { details });
159
+ return new ZitadelError(error.status === 401 || error.status === 403 ? "E_AUTH" : error.status >= 500 ? "E_NETWORK" : "E_VALIDATION", apiErrorMessage(error), { details });
160
+ }
161
+ if (isErrnoException(error)) {
162
+ const details = { original: pickErrorShape(error) };
163
+ if (error.code === "EACCES" || error.code === "EPERM") return new ZitadelError("E_AUTH", `Permission denied: ${error.message}`, {
164
+ hint: "Check file permissions or run with the right user.",
165
+ details
166
+ });
167
+ if (error.code === "EEXIST") return new ZitadelError("E_CONFLICT", error.message, {
168
+ hint: "A file already exists. Use --force to overwrite or remove it first.",
169
+ details
170
+ });
171
+ if (error.code === "ENOENT") return new ZitadelError("E_VALIDATION", error.message, {
172
+ hint: "A required file or directory is missing.",
173
+ details
174
+ });
175
+ }
176
+ if (isNetworkError(error)) return new ZitadelError("E_NETWORK", errorMessage(error), {
177
+ hint: "Check your connection, ZITADEL_API_BASE, or the configured server URL.",
178
+ details: { original: pickErrorShape(error) }
179
+ });
180
+ if (isZodLikeError(error)) return new ZitadelError("E_VALIDATION", errorMessage(error), { details: { issues: error.issues } });
181
+ if (error instanceof Error) return new ZitadelError("E_VALIDATION", error.message, { details: { original: pickErrorShape(error) } });
182
+ return new ZitadelError("E_VALIDATION", "Unknown error", { details: error });
183
+ }
184
+ /**
185
+ * True when a response body looks like the platform's structured error
186
+ * envelope (`{ code, message, … }`). Its presence proves the request reached
187
+ * a real Zitadel platform API rather than an arbitrary HTTP server.
188
+ */
189
+ function isPlatformErrorEnvelope(body) {
190
+ return typeof body === "object" && body !== null && typeof body.code === "string";
191
+ }
192
+ function isErrnoException(error) {
193
+ return error instanceof Error && typeof error.code === "string";
194
+ }
195
+ function isNetworkError(error) {
196
+ if (!(error instanceof Error)) return false;
197
+ if (error.name === "TypeError" && /fetch failed|network|ECONNREFUSED|ENOTFOUND/i.test(error.message)) return true;
198
+ const cause = error.cause;
199
+ if (cause && typeof cause === "object" && "code" in cause) {
200
+ const code = String(cause.code);
201
+ return /^(ECONNREFUSED|ECONNRESET|ENOTFOUND|ETIMEDOUT|EAI_AGAIN|UND_ERR)/i.test(code);
202
+ }
203
+ return false;
204
+ }
205
+ function isZodLikeError(error) {
206
+ return typeof error === "object" && error !== null && "issues" in error && Array.isArray(error.issues);
207
+ }
208
+ function errorMessage(error) {
209
+ if (error instanceof Error) return error.message;
210
+ if (typeof error === "string") return error;
211
+ return String(error);
212
+ }
213
+ function pickErrorShape(error) {
214
+ return {
215
+ name: error.name,
216
+ message: error.message,
217
+ code: error.code
218
+ };
219
+ }
220
+ //#endregion
221
+ //#region src/lib/json.ts
222
+ /**
223
+ * Serialise a value to pretty-printed JSON with object keys sorted at every
224
+ * depth. Determinism is the point: managed files written by the CLI must be
225
+ * byte-stable across runs so diffs stay clean and content hashes don't churn
226
+ * when only key ordering would otherwise differ. Delegates the deterministic
227
+ * sort to `safe-stable-stringify`, matching `JSON.stringify(value, null, 2)`
228
+ * formatting. The `?? "null"` only applies to `undefined`/function inputs,
229
+ * which the CLI never serialises.
230
+ */
231
+ function stableStringify(value) {
232
+ return stringify(value, null, 2) ?? "null";
233
+ }
234
+ /**
235
+ * Replace (or insert) one top-level key's value in a JSON document by
236
+ * splicing text, leaving every other byte of the source untouched. The
237
+ * counterpart of {@link stableStringify} for files the CLI does *not* own
238
+ * (the user's `package.json`): blank lines, inline nested objects, key
239
+ * order, indentation style, line endings, and the trailing newline all
240
+ * survive, because nothing outside the spliced value is reserialized. The
241
+ * replaced value itself is rendered at the document's detected indent (a
242
+ * compact document stays compact).
243
+ *
244
+ * Defensive by construction: the result is re-parsed and compared against
245
+ * the expected mutation, so a splice that would corrupt the document throws
246
+ * instead of writing it.
247
+ */
248
+ function setTopLevelJsonKey(source, path, key, value) {
249
+ const parsed = parseJsonObject(source, path);
250
+ const eol = source.includes("\r\n") ? "\r\n" : "\n";
251
+ const indent = detectIndent(source);
252
+ const rendered = renderValueAtDepthOne(value, indent, eol);
253
+ const layout = scanTopLevel(source);
254
+ const member = layout.members.find((candidate) => candidate.key === key);
255
+ let next;
256
+ if (member) next = `${source.slice(0, member.valueStart)}${rendered}${source.slice(member.valueEnd)}`;
257
+ else if (layout.members.length === 0) {
258
+ const inner = indent === 0 ? `${JSON.stringify(key)}:${rendered}` : `${eol}${indentString(indent)}${JSON.stringify(key)}: ${rendered}${eol}`;
259
+ next = `${source.slice(0, layout.open + 1)}${inner}${source.slice(layout.close)}`;
260
+ } else {
261
+ const last = layout.members[layout.members.length - 1];
262
+ const inner = indent === 0 ? `,${JSON.stringify(key)}:${rendered}` : `,${eol}${indentString(indent)}${JSON.stringify(key)}: ${rendered}`;
263
+ next = `${source.slice(0, last.valueEnd)}${inner}${source.slice(last.valueEnd)}`;
264
+ }
265
+ const verification = parseJsonObject(next, path);
266
+ const expected = {
267
+ ...parsed,
268
+ [key]: value
269
+ };
270
+ if (JSON.stringify(verification) !== JSON.stringify(expected)) throw new Error(`refusing to write ${path}: the targeted JSON edit did not verify`);
271
+ return next;
272
+ }
273
+ /**
274
+ * Infers the indentation unit of a JSON document: the whitespace prefix of
275
+ * its first indented line, a compact document stays compact (`0`), and
276
+ * anything unrecognizable falls back to two spaces.
277
+ */
278
+ function detectIndent(source) {
279
+ const match = source.match(/\n([ \t]+)\S/);
280
+ if (match?.[1]) return match[1];
281
+ return source.trimEnd().includes("\n") ? 2 : 0;
282
+ }
283
+ function indentString(indent) {
284
+ return typeof indent === "number" ? " ".repeat(indent) : indent;
285
+ }
286
+ /** Serializes a value as it should appear as a top-level member's value. */
287
+ function renderValueAtDepthOne(value, indent, eol) {
288
+ if (indent === 0) return JSON.stringify(value);
289
+ const unit = indentString(indent);
290
+ return JSON.stringify(value, null, unit).split("\n").join(`${eol}${unit}`);
291
+ }
292
+ /**
293
+ * Single-pass, string-aware scan of a JSON object document, collecting the
294
+ * exact text span of every top-level member's value plus the root braces.
295
+ * Only structural understanding needed for the splice — the document has
296
+ * already been validated by `JSON.parse`, so this can assume well-formed
297
+ * input and does not re-validate.
298
+ */
299
+ function scanTopLevel(source) {
300
+ const members = [];
301
+ let open = -1;
302
+ let close = -1;
303
+ let depth = 0;
304
+ let index = 0;
305
+ while (index < source.length) {
306
+ const char = source[index];
307
+ if (char === "\"") {
308
+ const start = index;
309
+ index = skipString(source, index);
310
+ if (depth === 1 && isKeyPosition(source, start)) {
311
+ const key = JSON.parse(source.slice(start, index));
312
+ while (source[index] !== ":") index += 1;
313
+ index += 1;
314
+ while (index < source.length && isWhitespace(source[index])) index += 1;
315
+ const valueStart = index;
316
+ index = skipValue(source, index);
317
+ members.push({
318
+ key,
319
+ valueStart,
320
+ valueEnd: index
321
+ });
322
+ }
323
+ continue;
324
+ }
325
+ if (char === "{" || char === "[") {
326
+ depth += 1;
327
+ if (depth === 1 && open === -1) open = index;
328
+ } else if (char === "}" || char === "]") {
329
+ depth -= 1;
330
+ if (depth === 0) close = index;
331
+ }
332
+ index += 1;
333
+ }
334
+ return {
335
+ open,
336
+ close,
337
+ members
338
+ };
339
+ }
340
+ /** True when the string starting at `start` is a top-level key, not a value. */
341
+ function isKeyPosition(source, start) {
342
+ for (let index = start - 1; index >= 0; index -= 1) {
343
+ const char = source[index];
344
+ if (isWhitespace(char)) continue;
345
+ return char === "{" || char === ",";
346
+ }
347
+ return false;
348
+ }
349
+ /** Returns the index just past the value starting at `index`. */
350
+ function skipValue(source, index) {
351
+ const char = source[index];
352
+ if (char === "\"") return skipString(source, index);
353
+ if (char === "{" || char === "[") {
354
+ let depth = 0;
355
+ let cursor = index;
356
+ while (cursor < source.length) {
357
+ const current = source[cursor];
358
+ if (current === "\"") {
359
+ cursor = skipString(source, cursor);
360
+ continue;
361
+ }
362
+ if (current === "{" || current === "[") depth += 1;
363
+ if (current === "}" || current === "]") {
364
+ depth -= 1;
365
+ if (depth === 0) return cursor + 1;
366
+ }
367
+ cursor += 1;
368
+ }
369
+ return cursor;
370
+ }
371
+ let cursor = index;
372
+ while (cursor < source.length && !",}]".includes(source[cursor]) && !isWhitespace(source[cursor])) cursor += 1;
373
+ return cursor;
374
+ }
375
+ /** Returns the index just past the closing quote of the string at `index`. */
376
+ function skipString(source, index) {
377
+ let cursor = index + 1;
378
+ while (cursor < source.length) {
379
+ const char = source[cursor];
380
+ if (char === "\\") {
381
+ cursor += 2;
382
+ continue;
383
+ }
384
+ if (char === "\"") return cursor + 1;
385
+ cursor += 1;
386
+ }
387
+ return cursor;
388
+ }
389
+ function isWhitespace(char) {
390
+ return char === " " || char === " " || char === "\n" || char === "\r";
391
+ }
392
+ /**
393
+ * Parse `contents` as JSON and assert the root is a plain object (not an
394
+ * array or scalar). The CLI's config and secret files are always objects, so
395
+ * this guards callers from the `JSON.parse` return type of `any` and produces
396
+ * a `path`-qualified error message pointing at the offending file.
397
+ */
398
+ function parseJsonObject(contents, path) {
399
+ const value = JSON.parse(contents);
400
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${path} must contain a JSON object`);
401
+ return value;
402
+ }
403
+ /**
404
+ * Narrows an unknown value to a plain (non-array, non-null) object. Shared by
405
+ * the commands and the file-writer that walk parsed JSON, so the predicate
406
+ * isn't reimplemented per call site.
407
+ */
408
+ function isObject(value) {
409
+ return typeof value === "object" && value !== null && !Array.isArray(value);
410
+ }
411
+ //#endregion
412
+ //#region src/lib/paths.ts
413
+ /**
414
+ * Resolve the working directory the CLI should operate against, defaulting to
415
+ * the process CWD when no `--cwd` override is given. Always returns an
416
+ * absolute path so downstream `join`/`readFile` calls are unaffected by later
417
+ * `process.chdir` or relative-path ambiguity.
418
+ */
419
+ function resolveCwd(cwd) {
420
+ return resolve(cwd ?? process.cwd());
421
+ }
422
+ /**
423
+ * Sentinel comment stamped at the top of every file the CLI generates and
424
+ * owns. Commands like `doctor` and `eject` look for this marker to decide
425
+ * whether a file is safe to touch; the trailing `v1` lets the format evolve
426
+ * without mistaking newer managed files for hand-edited ones.
427
+ */
428
+ const MANAGED_MARKER = "// zitadel-cli: managed-file v1";
429
+ //#endregion
430
+ //#region src/lib/local-server/runtime.ts
431
+ const LOCAL_SERVER_IMAGE_NAME = "ghcr.io/zitadel/nextgen";
432
+ const DEFAULT_LOCAL_SERVER_IMAGE = `${LOCAL_SERVER_IMAGE_NAME}:latest`;
433
+ const DEFAULT_LOCAL_SERVER_PORT = 8080;
434
+ const DEFAULT_LOCAL_SERVER_URL = "http://localhost:8080";
435
+ const LOCAL_RUNTIME_DIR = ".zitadel/local";
436
+ const LOCAL_DATA_DIR = ".zitadel/local/nextgen-data";
437
+ const LOCAL_RUNTIME_FILE = ".zitadel/local/runtime.json";
438
+ const LOCAL_SERVER_LOG_FILE = ".zitadel/local/server.log";
439
+ const LOCAL_CONTAINER_PASSWD_FILE = ".zitadel/local/container-passwd";
440
+ const LOCAL_CONTAINER_GROUP_FILE = ".zitadel/local/container-group";
441
+ const CONTAINER_DATA_DIR = "/var/lib/zitadel/nextgen-data";
442
+ const CONTAINER_HTTP_PORT = 8080;
443
+ function localRuntimePaths(cwd) {
444
+ return {
445
+ runtimeDir: join(cwd, LOCAL_RUNTIME_DIR),
446
+ dataDir: join(cwd, LOCAL_DATA_DIR),
447
+ runtimeFile: join(cwd, LOCAL_RUNTIME_FILE),
448
+ logFile: join(cwd, LOCAL_SERVER_LOG_FILE),
449
+ containerPasswdFile: join(cwd, LOCAL_CONTAINER_PASSWD_FILE),
450
+ containerGroupFile: join(cwd, LOCAL_CONTAINER_GROUP_FILE)
451
+ };
452
+ }
453
+ function localContainerName(cwd) {
454
+ return `zitadel-server-${createHash("sha256").update(resolve(cwd)).digest("hex").slice(0, 12)}`;
455
+ }
456
+ function localServerUrl(port) {
457
+ return `http://localhost:${port}`;
458
+ }
459
+ function defaultLocalServerImageForCliVersion(cliVersion) {
460
+ const normalized = cliVersion.trim().replace(/^v/, "");
461
+ if (/^\d+\.\d+\.\d+-alpha\.\d+$/.test(normalized)) return `${LOCAL_SERVER_IMAGE_NAME}:${normalized}`;
462
+ return DEFAULT_LOCAL_SERVER_IMAGE;
463
+ }
464
+ async function ensureLocalState(cwd) {
465
+ const paths = localRuntimePaths(cwd);
466
+ await mkdir(paths.dataDir, {
467
+ recursive: true,
468
+ mode: 448
469
+ });
470
+ await appendGitignoreEntry(cwd, `${LOCAL_RUNTIME_DIR}/`);
471
+ return paths;
472
+ }
473
+ async function assertLocalStateWritable(cwd) {
474
+ const paths = localRuntimePaths(cwd);
475
+ const checkedPath = await nearestExistingDirectory(paths.dataDir);
476
+ await access(checkedPath, constants.W_OK);
477
+ return {
478
+ targetPath: paths.dataDir,
479
+ checkedPath
480
+ };
481
+ }
482
+ async function ensureContainerIdentity(cwd, user) {
483
+ if (user.uid === void 0 || user.uid <= 0) return;
484
+ const gid = user.gid ?? user.uid;
485
+ const paths = localRuntimePaths(cwd);
486
+ await mkdir(paths.runtimeDir, {
487
+ recursive: true,
488
+ mode: 448
489
+ });
490
+ await writeFile(paths.containerPasswdFile, [
491
+ "root:x:0:0:root:/root:/bin/sh",
492
+ "nonroot:x:65532:65532:nonroot:/nonexistent:/usr/sbin/nologin",
493
+ `zitadel-local:x:${String(user.uid)}:${String(gid)}:Zitadel local user:/tmp:/usr/sbin/nologin`,
494
+ ""
495
+ ].join("\n"), { mode: 420 });
496
+ await writeFile(paths.containerGroupFile, [
497
+ "root:x:0:",
498
+ "nonroot:x:65532:",
499
+ `zitadel-local:x:${String(gid)}:`,
500
+ ""
501
+ ].join("\n"), { mode: 420 });
502
+ return {
503
+ uid: user.uid,
504
+ gid,
505
+ passwdFile: paths.containerPasswdFile,
506
+ groupFile: paths.containerGroupFile
507
+ };
508
+ }
509
+ async function readRuntimeMetadata(cwd) {
510
+ const paths = localRuntimePaths(cwd);
511
+ let raw;
512
+ try {
513
+ raw = await readFile(paths.runtimeFile, "utf8");
514
+ } catch (error) {
515
+ if (isErrno(error, "ENOENT")) return;
516
+ throw error;
517
+ }
518
+ return normalizeRuntimeMetadata(parseJsonObject(raw, LOCAL_RUNTIME_FILE));
519
+ }
520
+ async function writeRuntimeMetadata(cwd, metadata) {
521
+ const paths = localRuntimePaths(cwd);
522
+ await mkdir(paths.runtimeDir, {
523
+ recursive: true,
524
+ mode: 448
525
+ });
526
+ await writeFile(paths.runtimeFile, `${JSON.stringify(metadata, null, 2)}\n`, { mode: 384 });
527
+ }
528
+ async function removeRuntimeMetadata(cwd) {
529
+ await rm(localRuntimePaths(cwd).runtimeFile, { force: true });
530
+ }
531
+ async function removeLocalData(cwd) {
532
+ await rm(localRuntimePaths(cwd).dataDir, {
533
+ recursive: true,
534
+ force: true
535
+ });
536
+ }
537
+ async function checkLocalServerHealth(serverUrl, timeoutMs = 1500) {
538
+ try {
539
+ const healthUrl = new URL("/healthz", serverUrl);
540
+ return (await fetch(healthUrl, { signal: AbortSignal.timeout(timeoutMs) })).ok;
541
+ } catch {
542
+ return false;
543
+ }
544
+ }
545
+ /**
546
+ * Best-effort local-server detection for optional UI (the setup wizard's
547
+ * server choice). Same sources as {@link resolveLocalServer} — the runtime
548
+ * metadata written by `zitadel start`, then the default localhost URL — but
549
+ * never throws: a malformed `runtime.json` or an unhealthy server yields
550
+ * `undefined` (doctor owns diagnosing those states), and an unhealthy
551
+ * metadata URL still falls back to the default-port probe so a server
552
+ * started from a different directory is found.
553
+ */
554
+ async function detectHealthyLocalServer(cwd) {
555
+ let runtime;
556
+ try {
557
+ runtime = await readRuntimeMetadata(cwd);
558
+ } catch {
559
+ runtime = void 0;
560
+ }
561
+ if (runtime && await checkLocalServerHealth(runtime.server_url)) return runtime.server_url;
562
+ if (!(runtime?.server_url === "http://localhost:8080") && await checkLocalServerHealth("http://localhost:8080")) return DEFAULT_LOCAL_SERVER_URL;
563
+ }
564
+ async function resolveLocalServer(cwd) {
565
+ const runtime = await readRuntimeMetadata(cwd);
566
+ if (runtime) {
567
+ if (await checkLocalServerHealth(runtime.server_url)) return runtime.server_url;
568
+ throw localServerNotRunning(runtime.server_url);
569
+ }
570
+ if (await checkLocalServerHealth("http://localhost:8080")) return DEFAULT_LOCAL_SERVER_URL;
571
+ throw localServerNotRunning(DEFAULT_LOCAL_SERVER_URL);
572
+ }
573
+ function localServerNotRunning(serverUrl) {
574
+ return new ZitadelError("E_LOCAL_SERVER_NOT_RUNNING", "Local Zitadel server is not running", {
575
+ hint: `No healthy local server responded at ${serverUrl}.`,
576
+ nextCommands: ["zitadel start"],
577
+ details: { server_url: serverUrl }
578
+ });
579
+ }
580
+ async function appendGitignoreEntry(cwd, entry) {
581
+ const path = join(cwd, ".gitignore");
582
+ let existing = "";
583
+ try {
584
+ existing = await readFile(path, "utf8");
585
+ } catch (error) {
586
+ if (!isErrno(error, "ENOENT")) throw error;
587
+ }
588
+ if (existing.split(/\r?\n/).map((line) => line.trim()).includes(entry)) return;
589
+ const prefix = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
590
+ await writeFile(path, `${existing}${prefix}${entry}\n`);
591
+ }
592
+ function normalizeRuntimeMetadata(input) {
593
+ if (input.schema_version !== 1 || typeof input.port !== "number" || !isValidPort(input.port) || typeof input.server_url !== "string" || !isValidServerUrl(input.server_url, input.port) || typeof input.data_dir !== "string" || typeof input.created_at !== "string" || typeof input.cli_version !== "string") throw malformedRuntime(input);
594
+ const backend = input.backend === void 0 ? "docker" : input.backend;
595
+ const base = {
596
+ schema_version: 1,
597
+ port: input.port,
598
+ server_url: input.server_url,
599
+ data_dir: input.data_dir,
600
+ created_at: input.created_at,
601
+ cli_version: input.cli_version
602
+ };
603
+ if (backend === "binary") {
604
+ if (typeof input.pid !== "number" || !Number.isInteger(input.pid) || input.pid <= 0 || typeof input.command !== "string" || typeof input.log_path !== "string" || typeof input.server_package !== "string" || typeof input.server_version !== "string") throw malformedRuntime(input);
605
+ return {
606
+ ...base,
607
+ backend: "binary",
608
+ pid: input.pid,
609
+ command: input.command,
610
+ log_path: input.log_path,
611
+ server_package: input.server_package,
612
+ server_version: input.server_version
613
+ };
614
+ }
615
+ if (backend !== "docker" || typeof input.container_name !== "string" || typeof input.container_id !== "string" || typeof input.image !== "string") throw malformedRuntime(input);
616
+ return {
617
+ ...base,
618
+ backend: "docker",
619
+ container_name: input.container_name,
620
+ container_id: input.container_id,
621
+ image: input.image
622
+ };
623
+ }
624
+ async function nearestExistingDirectory(path) {
625
+ let current = path;
626
+ while (true) try {
627
+ if (!(await stat(current)).isDirectory()) throw new Error(`${current} exists but is not a directory`);
628
+ return current;
629
+ } catch (error) {
630
+ if (!isErrno(error, "ENOENT")) throw error;
631
+ const parent = dirname(current);
632
+ if (parent === current) throw error;
633
+ current = parent;
634
+ }
635
+ }
636
+ function isErrno(error, code) {
637
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
638
+ }
639
+ function runtimeSummary(metadata) {
640
+ if (!metadata) return { configured: false };
641
+ const base = {
642
+ configured: true,
643
+ backend: metadata.backend,
644
+ port: metadata.port,
645
+ server_url: metadata.server_url,
646
+ data_dir: metadata.data_dir,
647
+ created_at: metadata.created_at
648
+ };
649
+ if (metadata.backend === "binary") return {
650
+ ...base,
651
+ pid: metadata.pid,
652
+ command: metadata.command,
653
+ log_path: metadata.log_path,
654
+ server_package: metadata.server_package,
655
+ server_version: metadata.server_version
656
+ };
657
+ return {
658
+ ...base,
659
+ container_name: metadata.container_name,
660
+ container_id: metadata.container_id,
661
+ image: metadata.image
662
+ };
663
+ }
664
+ function isValidPort(value) {
665
+ return Number.isInteger(value) && value >= 1 && value <= 65535;
666
+ }
667
+ function isValidServerUrl(value, port) {
668
+ try {
669
+ const url = new URL(value);
670
+ return (url.protocol === "http:" || url.protocol === "https:") && url.hostname.length > 0 && explicitUrlPort(value) === port;
671
+ } catch {
672
+ return false;
673
+ }
674
+ }
675
+ function explicitUrlPort(value) {
676
+ const match = value.match(/^[a-z][a-z\d+\-.]*:\/\/(?:\[[^\]]+\]|[^/?#:]+):(\d+)(?:[/?#]|$)/i);
677
+ if (!match) return;
678
+ const port = Number(match[1]);
679
+ return isValidPort(port) ? port : void 0;
680
+ }
681
+ function malformedRuntime(input) {
682
+ return new ZitadelError("E_VALIDATION", `${LOCAL_RUNTIME_FILE} is malformed`, {
683
+ hint: "Run `zitadel reset --force`, then `zitadel start`.",
684
+ nextCommands: ["zitadel reset --force", "zitadel start"],
685
+ details: input
686
+ });
687
+ }
688
+ //#endregion
689
+ //#region src/lib/server.ts
690
+ /**
691
+ * Server URL used when nothing else resolves. Also surfaced in hints and
692
+ * the interactive setup prompt as the suggested value, so it is exported
693
+ * rather than kept private.
694
+ */
695
+ const DEFAULT_SERVER = "https://api.zitadel.cloud";
696
+ /**
697
+ * Resolves which server the CLI should target, applying a fixed
698
+ * precedence: explicit `--server` flag, then `ZITADEL_API_BASE`, then the
699
+ * selected environment block in `zitadel.json`, then the config's
700
+ * top-level `server`, falling back to {@link DEFAULT_SERVER}. Every
701
+ * candidate is validated to a normalised origin; an invalid URL throws a
702
+ * `ZitadelError` rather than silently falling through.
703
+ */
704
+ async function resolveServer(input) {
705
+ if (input.serverFlag) return validate(input.cwd, {
706
+ value: input.serverFlag,
707
+ origin: "flag"
708
+ });
709
+ const envValue = input.env.ZITADEL_API_BASE;
710
+ if (envValue) return validate(input.cwd, {
711
+ value: envValue,
712
+ origin: "env"
713
+ });
714
+ const config = await readConfig(input.cwd);
715
+ if (config) {
716
+ const envBranch = readEnvServer(config, input.environment);
717
+ if (envBranch) return validate(input.cwd, {
718
+ value: envBranch,
719
+ origin: "config-env"
720
+ });
721
+ if (typeof config.server === "string") return validate(input.cwd, {
722
+ value: config.server,
723
+ origin: "config-top"
724
+ });
725
+ }
726
+ return {
727
+ value: DEFAULT_SERVER,
728
+ origin: "default"
729
+ };
730
+ }
731
+ async function validate(cwd, resolved) {
732
+ if (resolved.value === "local") return {
733
+ value: await resolveLocalServer(cwd),
734
+ origin: "local"
735
+ };
736
+ try {
737
+ const url = new URL(resolved.value);
738
+ 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}.` });
739
+ return {
740
+ value: url.origin,
741
+ origin: resolved.origin
742
+ };
743
+ } catch (error) {
744
+ if (error instanceof ZitadelError) throw error;
745
+ throw new ZitadelError("E_VALIDATION", `Invalid server "${resolved.value}"`, {
746
+ hint: `Use a URL like ${DEFAULT_SERVER}.`,
747
+ details: { origin: resolved.origin }
748
+ });
749
+ }
750
+ }
751
+ async function readConfig(cwd) {
752
+ try {
753
+ return parseJsonObject(await readFile(join(cwd, "zitadel.json"), "utf8"), "zitadel.json");
754
+ } catch (error) {
755
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return;
756
+ throw error;
757
+ }
758
+ }
759
+ function readEnvServer(config, environment) {
760
+ if (!environment) return;
761
+ const envs = config.environments;
762
+ if (!isObject(envs)) return;
763
+ const branch = envs[environment];
764
+ if (!isObject(branch)) return;
765
+ return typeof branch.server === "string" ? branch.server : void 0;
766
+ }
767
+ //#endregion
768
+ //#region src/lib/telemetry/config.ts
769
+ /**
770
+ * Resolves the Mixpanel ingestion token and API host for a CLI invocation.
771
+ *
772
+ * The token is a *write-only* project token: it can ingest events but cannot
773
+ * read data back, so — unlike the project service-key — it is safe to ship
774
+ * inside the published CLI. This mirrors how Next.js, Astro, and other dev
775
+ * tools embed their telemetry token, and is the only workable model for a CLI
776
+ * (we cannot ask end users to supply one). It is intentionally not a secret.
777
+ *
778
+ * Dev and prod are separate Mixpanel projects (the skill's Phase 2 rule: never
779
+ * track dev traffic into the production project). By default the channel comes
780
+ * from a build-time stamp (see {@link resolveChannel}), so the published CLI
781
+ * routes real user traffic to production without any per-user env var while
782
+ * source/test runs stay on dev — but a runtime `ZITADEL_TELEMETRY_ENV` or
783
+ * `ZITADEL_TELEMETRY_BUILD_CHANNEL` overrides the stamp, and
784
+ * `ZITADEL_TELEMETRY_TOKEN` overrides the token outright.
785
+ */
786
+ /**
787
+ * Development project token. Safe to commit (write-only ingestion key). Used
788
+ * when running from source or any non-production build.
789
+ */
790
+ const DEV_TELEMETRY_TOKEN = "0fb432b08a9797b87b0eebcbee11706e";
791
+ /**
792
+ * Production project token. Used by the published CLI (the build stamps the
793
+ * production channel) and any `ZITADEL_TELEMETRY_ENV=production` run. Write-only
794
+ * ingestion key, like the dev token — safe to commit.
795
+ */
796
+ const PROD_TELEMETRY_TOKEN = "f56fd7315ccd614fba8eecb2a8966152";
797
+ /** Mixpanel API hosts by data-residency region. */
798
+ const HOSTS = {
799
+ us: "api.mixpanel.com",
800
+ eu: "api-eu.mixpanel.com"
801
+ };
802
+ /**
803
+ * Channel stamped into the bundle at build time. tsdown's `define` always
804
+ * replaces the bare `__ZITADEL_TELEMETRY_CHANNEL__` identifier — with
805
+ * `"development"` by default and `"production"` only in the release build — so
806
+ * the shipped CLI routes to the right project with no per-user env var. The
807
+ * identifier is undefined only in unbundled runs (e.g. unit tests importing this
808
+ * module directly); the `typeof` guard returns `""` there so those runs fall
809
+ * through to the dev default without a ReferenceError.
810
+ */
811
+ function buildStampedChannel() {
812
+ return "production".trim().toLowerCase();
813
+ }
814
+ /**
815
+ * Decide which project the events belong to. Precedence: an explicit
816
+ * `ZITADEL_TELEMETRY_ENV`, then a `ZITADEL_TELEMETRY_BUILD_CHANNEL` env override
817
+ * (handy for CI/release), then the build-time channel stamp. The default —
818
+ * source/dev/test — is the dev project. Ambient `NODE_ENV` is deliberately NOT
819
+ * consulted: a source build with `NODE_ENV=production` must not route dev
820
+ * traffic to prod, nor a published run with `NODE_ENV=development` to dev.
821
+ */
822
+ function resolveChannel(env) {
823
+ const explicit = (env.ZITADEL_TELEMETRY_ENV ?? "").trim().toLowerCase();
824
+ if (explicit === "production") return "production";
825
+ if (explicit === "development") return "development";
826
+ return (env.ZITADEL_TELEMETRY_BUILD_CHANNEL ?? buildStampedChannel()).trim().toLowerCase() === "production" ? "production" : "development";
827
+ }
828
+ /**
829
+ * Resolve the ingestion token, or `undefined` when none is configured for the
830
+ * active channel. A `ZITADEL_TELEMETRY_TOKEN` override wins outright; otherwise
831
+ * the channel's baked token is used. An empty string (e.g. the unset prod
832
+ * token) resolves to `undefined`, which the caller treats as "telemetry off".
833
+ */
834
+ function resolveTelemetryToken(env) {
835
+ const override = env.ZITADEL_TELEMETRY_TOKEN?.trim();
836
+ if (override) return override;
837
+ const token = resolveChannel(env) === "production" ? PROD_TELEMETRY_TOKEN : DEV_TELEMETRY_TOKEN;
838
+ return token.length > 0 ? token : void 0;
839
+ }
840
+ /**
841
+ * Resolve the Mixpanel API host from `ZITADEL_TELEMETRY_REGION`. Defaults to the
842
+ * EU host, because the Zitadel Mixpanel projects live in the EU data-residency
843
+ * region. Set `ZITADEL_TELEMETRY_REGION=us` for a US-hosted project — events
844
+ * sent to the wrong host are silently dropped, so verify the first event lands
845
+ * in Live View.
846
+ */
847
+ function resolveTelemetryHost(env) {
848
+ return (env.ZITADEL_TELEMETRY_REGION ?? "eu").trim().toLowerCase() === "us" ? HOSTS.us : HOSTS.eu;
849
+ }
850
+ //#endregion
851
+ //#region src/lib/telemetry/consent.ts
852
+ const DISABLED_VALUES$1 = new Set([
853
+ "",
854
+ "0",
855
+ "false",
856
+ "off",
857
+ "no"
858
+ ]);
859
+ /**
860
+ * Resolve telemetry consent under an opt-out model: on by default, but any of
861
+ * several explicit signals turns it off, in precedence order.
862
+ *
863
+ * 1. `--no-telemetry` on the command line — the most explicit, per-invocation.
864
+ * 2. An automated test run (`VITEST`/`NODE_ENV=test`) — never emit synthetic
865
+ * traffic or pay the shutdown flush; spawned CLI subprocesses inherit it.
866
+ * 3. `DO_NOT_TRACK` — the cross-tool standard (https://consoledonottrack.com);
867
+ * any value other than `0`/empty disables.
868
+ * 4. `ZITADEL_TELEMETRY` set to a falsey token (`0`/`false`/`off`/`no`).
869
+ * 5. No ingestion token configured for the active channel — nothing to send to,
870
+ * so telemetry is inert regardless of consent.
871
+ *
872
+ * Consent being enabled does not by itself send anything; the caller still
873
+ * builds the client lazily and fails open on any transport error.
874
+ */
875
+ function resolveConsent(input) {
876
+ if (input.flag === false) return {
877
+ enabled: false,
878
+ reason: "flag-opt-out"
879
+ };
880
+ if (input.env.VITEST || input.env.NODE_ENV === "test") return {
881
+ enabled: false,
882
+ reason: "test-runner"
883
+ };
884
+ const doNotTrack = input.env.DO_NOT_TRACK?.trim();
885
+ if (doNotTrack && doNotTrack !== "0") return {
886
+ enabled: false,
887
+ reason: "do-not-track"
888
+ };
889
+ const explicit = input.env.ZITADEL_TELEMETRY?.trim().toLowerCase();
890
+ if (explicit !== void 0 && DISABLED_VALUES$1.has(explicit)) return {
891
+ enabled: false,
892
+ reason: "env-opt-out"
893
+ };
894
+ const token = resolveTelemetryToken(input.env);
895
+ if (!token) return {
896
+ enabled: false,
897
+ reason: "no-token"
898
+ };
899
+ return {
900
+ enabled: true,
901
+ reason: "enabled",
902
+ token
903
+ };
904
+ }
905
+ //#endregion
906
+ //#region src/lib/telemetry/identity.ts
907
+ /**
908
+ * Resolve the directory the CLI persists cross-invocation state in, honoring
909
+ * the platform conventions: `XDG_CONFIG_HOME` then `~/.config` on Unix, and
910
+ * `%APPDATA%` on Windows. The anonymous id lives here so it survives between
911
+ * runs without touching the user's project tree.
912
+ */
913
+ function telemetryConfigDir(env) {
914
+ if (process.platform === "win32" && env.APPDATA) return join(env.APPDATA, "zitadel", "cli");
915
+ return join(env.XDG_CONFIG_HOME?.trim() || join(homedir(), ".config"), "zitadel", "cli");
916
+ }
917
+ /**
918
+ * Load the persisted anonymous id, minting and storing a new one on first run.
919
+ *
920
+ * Fail-open: if the config dir cannot be resolved, read, or written — including
921
+ * an `os.homedir()` throw in a restricted/containerized environment, a
922
+ * read-only home, a CI sandbox, or a permissions error — fall back to an
923
+ * ephemeral per-process id and report `isFirstRun: false` so we neither crash
924
+ * nor nag the user with the notice on every run. Telemetry is best-effort,
925
+ * never load-bearing.
926
+ */
927
+ function loadOrCreateIdentity(env) {
928
+ let dir;
929
+ try {
930
+ dir = telemetryConfigDir(env);
931
+ } catch {
932
+ return {
933
+ distinctId: randomUUID(),
934
+ isFirstRun: false
935
+ };
936
+ }
937
+ const file = join(dir, "telemetry.json");
938
+ try {
939
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
940
+ if (typeof parsed.distinctId === "string" && parsed.distinctId.length > 0) return {
941
+ distinctId: parsed.distinctId,
942
+ isFirstRun: false
943
+ };
944
+ } catch {}
945
+ const distinctId = randomUUID();
946
+ try {
947
+ mkdirSync(dir, { recursive: true });
948
+ writeFileSync(file, `${JSON.stringify({ distinctId })}\n`, { mode: 384 });
949
+ return {
950
+ distinctId,
951
+ isFirstRun: true
952
+ };
953
+ } catch {
954
+ return {
955
+ distinctId,
956
+ isFirstRun: false
957
+ };
958
+ }
959
+ }
960
+ //#endregion
961
+ //#region src/lib/telemetry/util.ts
962
+ /**
963
+ * Return a new bag with empty values stripped, per Mixpanel's "omit, never send
964
+ * null/''" rule. Pure — the input is never mutated.
965
+ */
966
+ function compact(properties) {
967
+ return Object.fromEntries(Object.entries(properties).filter(([, value]) => value !== void 0 && value !== null && value !== ""));
968
+ }
969
+ //#endregion
970
+ //#region src/lib/telemetry/index.ts
971
+ /**
972
+ * A generic, application-agnostic Mixpanel client wrapper for a single short
973
+ * process. It knows nothing about commands, CLIs, or event names — callers
974
+ * supply fully-built property bags. Its only opinions are operational, because
975
+ * telemetry must never degrade the host program:
976
+ *
977
+ * - Never throws — a missing token, opt-out, or transport error all degrade to
978
+ * a silent no-op.
979
+ * - Never blocks beyond {@link shutdown}'s timeout.
980
+ * - Never writes to stdout; the optional debug trace goes to stderr.
981
+ *
982
+ * Inputs are treated as immutable: {@link track}/{@link profile} build a new
983
+ * payload via spread and never mutate the bag they are given.
984
+ */
985
+ var Telemetry = class Telemetry {
986
+ pending = [];
987
+ constructor(client, distinctId, isFirstRun, debug, newId) {
988
+ this.client = client;
989
+ this.distinctId = distinctId;
990
+ this.isFirstRun = isFirstRun;
991
+ this.debug = debug;
992
+ this.newId = newId;
993
+ }
994
+ /** Whether events will actually be sent (consent granted and a token configured). */
995
+ get enabled() {
996
+ return this.client !== void 0;
997
+ }
998
+ /**
999
+ * Resolve consent, identity, token, and host, then build the Mixpanel client
1000
+ * lazily — only when consent is granted *and* a token is configured. Any
1001
+ * failure along the way yields an inert instance whose methods are no-ops.
1002
+ */
1003
+ static create(deps) {
1004
+ const newId = deps.newId ?? randomUUID;
1005
+ const debug = deps.debug ?? false;
1006
+ const inert = (firstRun) => new Telemetry(void 0, "", firstRun, debug, newId);
1007
+ const consent = resolveConsent({
1008
+ env: deps.env,
1009
+ flag: deps.flag
1010
+ });
1011
+ if (debug) process.stderr.write(`[telemetry] consent: ${consent.reason}\n`);
1012
+ if (!consent.enabled || !consent.token) return inert(false);
1013
+ const identity = (deps.loadIdentity ?? loadOrCreateIdentity)(deps.env);
1014
+ let client;
1015
+ try {
1016
+ client = (deps.initClient ?? defaultInit)(consent.token, resolveTelemetryHost(deps.env));
1017
+ } catch {
1018
+ return inert(identity.isFirstRun);
1019
+ }
1020
+ return new Telemetry(client, identity.distinctId, identity.isFirstRun, debug, newId);
1021
+ }
1022
+ /** Send an event with the given properties; `distinct_id`/`$insert_id` are stamped here. */
1023
+ track(event, properties) {
1024
+ if (!this.client) return;
1025
+ const payload = compact({
1026
+ ...properties,
1027
+ distinct_id: this.distinctId,
1028
+ $insert_id: this.newId()
1029
+ });
1030
+ if (this.debug) process.stderr.write(`[telemetry] ${event} ${JSON.stringify(payload)}\n`);
1031
+ this.enqueue((client, done) => client.track(event, payload, done));
1032
+ }
1033
+ /** Write a user profile via the People API. `modifiers` carries `$ip` etc. */
1034
+ profile(properties, modifiers = {}) {
1035
+ if (!this.client) return;
1036
+ const payload = compact(properties);
1037
+ if (this.debug) process.stderr.write(`[telemetry] people.set ${JSON.stringify(payload)}\n`);
1038
+ this.enqueue((client, done) => client.people.set(this.distinctId, payload, modifiers, done));
1039
+ }
1040
+ /**
1041
+ * Await in-flight sends so a short-lived process does not exit before the
1042
+ * requests complete, bounded by `timeoutMs`. The timeout is unref'd so the
1043
+ * losing race branch never holds the event loop open or needs a manual clear.
1044
+ * Safe to call on an inert instance.
1045
+ */
1046
+ async shutdown(timeoutMs = 2e3) {
1047
+ if (this.pending.length === 0) return;
1048
+ await Promise.race([Promise.allSettled(this.pending), setTimeout$1(timeoutMs, void 0, { ref: false })]);
1049
+ }
1050
+ /**
1051
+ * Enqueue one fire-and-forget send. Resolves (never rejects) on any failure —
1052
+ * a synchronous throw or an error callback must not surface to the caller or
1053
+ * leak as an unhandled rejection.
1054
+ */
1055
+ enqueue(send) {
1056
+ const client = this.client;
1057
+ if (!client) return;
1058
+ this.pending.push(new Promise((resolve) => {
1059
+ try {
1060
+ send(client, () => resolve());
1061
+ } catch {
1062
+ resolve();
1063
+ }
1064
+ }));
1065
+ }
1066
+ };
1067
+ /** Real Mixpanel client construction, isolated so {@link Telemetry.create} can swap it in tests. */
1068
+ function defaultInit(token, host) {
1069
+ return mixpanelLib.init(token, {
1070
+ host,
1071
+ geolocate: false
1072
+ });
1073
+ }
1074
+ //#endregion
1075
+ //#region src/lib/telemetry/dimensions/env-flag.ts
1076
+ const DISABLED_VALUES = new Set([
1077
+ "",
1078
+ "0",
1079
+ "false",
1080
+ "off",
1081
+ "no"
1082
+ ]);
1083
+ /**
1084
+ * Whether an environment variable is set to an enabled value. A present-but-
1085
+ * falsey string (`CI=false`, `CI=0`) counts as disabled — unlike a bare
1086
+ * `Boolean(env.CI)` check, which is true for any non-empty string.
1087
+ */
1088
+ function envEnabled(value) {
1089
+ return value !== void 0 && !DISABLED_VALUES.has(value.trim().toLowerCase());
1090
+ }
1091
+ //#endregion
1092
+ //#region src/lib/telemetry/dimensions/ci-flag.ts
1093
+ /** Whether the process is running inside an automated CI environment. */
1094
+ var CiFlag = class {
1095
+ value(env) {
1096
+ return envEnabled(env.CI) || envEnabled(env.GITHUB_ACTIONS) || envEnabled(env.GITLAB_CI);
1097
+ }
1098
+ };
1099
+ const ciFlag = new CiFlag();
1100
+ const ciProvider = new class CiProvider {
1101
+ /** Provider-marker env var → reported name, in match order. */
1102
+ static providers = [
1103
+ ["GITHUB_ACTIONS", "github_actions"],
1104
+ ["GITLAB_CI", "gitlab_ci"],
1105
+ ["CIRCLECI", "circleci"],
1106
+ ["BUILDKITE", "buildkite"],
1107
+ ["JENKINS_URL", "jenkins"]
1108
+ ];
1109
+ value(env) {
1110
+ const named = CiProvider.providers.find(([marker]) => envEnabled(env[marker]));
1111
+ if (named) return named[1];
1112
+ return envEnabled(env.CI) ? "unknown" : void 0;
1113
+ }
1114
+ }();
1115
+ //#endregion
1116
+ //#region src/lib/telemetry/dimensions/country.ts
1117
+ /**
1118
+ * ISO 3166-1 alpha-2 country for an IANA timezone, defaulting to the machine's
1119
+ * own zone. A curated subset of common zones — any zone not listed (or an
1120
+ * unknown/unavailable one) yields `undefined`, since we report no country
1121
+ * rather than guess. Derived from the timezone, never the IP, so no city or
1122
+ * region is ever inferred. The machine's own zone is resolved once and cached:
1123
+ * `Intl.DateTimeFormat` construction loads ICU data and is process-stable.
1124
+ */
1125
+ var Country = class {
1126
+ byTimezone = {
1127
+ "Africa/Abidjan": "CI",
1128
+ "Africa/Accra": "GH",
1129
+ "Africa/Addis_Ababa": "ET",
1130
+ "Africa/Algiers": "DZ",
1131
+ "Africa/Cairo": "EG",
1132
+ "Africa/Casablanca": "MA",
1133
+ "Africa/Johannesburg": "ZA",
1134
+ "Africa/Lagos": "NG",
1135
+ "Africa/Nairobi": "KE",
1136
+ "Africa/Tunis": "TN",
1137
+ "America/Anchorage": "US",
1138
+ "America/Argentina/Buenos_Aires": "AR",
1139
+ "America/Bogota": "CO",
1140
+ "America/Chicago": "US",
1141
+ "America/Denver": "US",
1142
+ "America/Halifax": "CA",
1143
+ "America/Lima": "PE",
1144
+ "America/Los_Angeles": "US",
1145
+ "America/Mexico_City": "MX",
1146
+ "America/New_York": "US",
1147
+ "America/Phoenix": "US",
1148
+ "America/Santiago": "CL",
1149
+ "America/Sao_Paulo": "BR",
1150
+ "America/Toronto": "CA",
1151
+ "America/Vancouver": "CA",
1152
+ "Asia/Bangkok": "TH",
1153
+ "Asia/Dhaka": "BD",
1154
+ "Asia/Dubai": "AE",
1155
+ "Asia/Hong_Kong": "HK",
1156
+ "Asia/Jakarta": "ID",
1157
+ "Asia/Jerusalem": "IL",
1158
+ "Asia/Karachi": "PK",
1159
+ "Asia/Kolkata": "IN",
1160
+ "Asia/Kuala_Lumpur": "MY",
1161
+ "Asia/Manila": "PH",
1162
+ "Asia/Riyadh": "SA",
1163
+ "Asia/Seoul": "KR",
1164
+ "Asia/Shanghai": "CN",
1165
+ "Asia/Singapore": "SG",
1166
+ "Asia/Taipei": "TW",
1167
+ "Asia/Tehran": "IR",
1168
+ "Asia/Tokyo": "JP",
1169
+ "Australia/Adelaide": "AU",
1170
+ "Australia/Brisbane": "AU",
1171
+ "Australia/Melbourne": "AU",
1172
+ "Australia/Perth": "AU",
1173
+ "Australia/Sydney": "AU",
1174
+ "Europe/Amsterdam": "NL",
1175
+ "Europe/Athens": "GR",
1176
+ "Europe/Berlin": "DE",
1177
+ "Europe/Brussels": "BE",
1178
+ "Europe/Bucharest": "RO",
1179
+ "Europe/Budapest": "HU",
1180
+ "Europe/Copenhagen": "DK",
1181
+ "Europe/Dublin": "IE",
1182
+ "Europe/Helsinki": "FI",
1183
+ "Europe/Istanbul": "TR",
1184
+ "Europe/Kyiv": "UA",
1185
+ "Europe/Lisbon": "PT",
1186
+ "Europe/London": "GB",
1187
+ "Europe/Madrid": "ES",
1188
+ "Europe/Moscow": "RU",
1189
+ "Europe/Oslo": "NO",
1190
+ "Europe/Paris": "FR",
1191
+ "Europe/Prague": "CZ",
1192
+ "Europe/Rome": "IT",
1193
+ "Europe/Stockholm": "SE",
1194
+ "Europe/Vienna": "AT",
1195
+ "Europe/Warsaw": "PL",
1196
+ "Europe/Zurich": "CH",
1197
+ "Pacific/Auckland": "NZ",
1198
+ "Pacific/Honolulu": "US"
1199
+ };
1200
+ machineZone;
1201
+ machineZoneResolved = false;
1202
+ value(timezone) {
1203
+ const zone = timezone ?? this.resolveMachineZone();
1204
+ return zone ? this.byTimezone[zone] : void 0;
1205
+ }
1206
+ resolveMachineZone() {
1207
+ if (!this.machineZoneResolved) {
1208
+ this.machineZoneResolved = true;
1209
+ try {
1210
+ this.machineZone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
1211
+ } catch {
1212
+ this.machineZone = void 0;
1213
+ }
1214
+ }
1215
+ return this.machineZone;
1216
+ }
1217
+ };
1218
+ const country = new Country();
1219
+ const hostAgent = new class HostAgent {
1220
+ /** Predicate → reported name, in match order. */
1221
+ static agents = [
1222
+ [(env) => Boolean(env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT), "claude_code"],
1223
+ [(env) => Boolean(env.CURSOR_TRACE_ID || env.CURSOR_AGENT), "cursor"],
1224
+ [(env) => env.TERM_PROGRAM === "vscode", "vscode"]
1225
+ ];
1226
+ value(env) {
1227
+ return HostAgent.agents.find(([matches]) => matches(env))?.[1] ?? "unknown";
1228
+ }
1229
+ }();
1230
+ const invocationChannel = new class InvocationChannel {
1231
+ /** Ordered so a more specific name wins — `pnpm` is matched before the `npm` prefix. */
1232
+ static managers = [
1233
+ "pnpm",
1234
+ "yarn",
1235
+ "bun",
1236
+ "npm"
1237
+ ];
1238
+ value(env) {
1239
+ const userAgent = env.npm_config_user_agent ?? "";
1240
+ return InvocationChannel.managers.find((manager) => userAgent.startsWith(manager)) ?? "unknown";
1241
+ }
1242
+ }();
1243
+ const operatingSystem = new class OperatingSystem {
1244
+ static labels = {
1245
+ darwin: "Mac OS X",
1246
+ win32: "Windows",
1247
+ linux: "Linux",
1248
+ freebsd: "BSD",
1249
+ openbsd: "BSD",
1250
+ netbsd: "BSD",
1251
+ aix: "AIX",
1252
+ sunos: "Solaris"
1253
+ };
1254
+ value(platform) {
1255
+ return OperatingSystem.labels[platform] ?? platform;
1256
+ }
1257
+ }();
1258
+ //#endregion
1259
+ //#region src/lib/oclif/command-telemetry.ts
1260
+ /**
1261
+ * CLI-specific telemetry glue: the only place that turns a {@link GlobalOptions}
1262
+ * invocation into the property bags the generic `Telemetry` client sends,
1263
+ * keeping `lib/telemetry` free of CLI coupling.
1264
+ */
1265
+ const CLI_COMMAND_STARTED = "cli_command_started";
1266
+ const CLI_COMMAND_COMPLETED = "cli_command_completed";
1267
+ const CLI_COMMAND_FAILED = "cli_command_failed";
1268
+ const FIRST_RUN_NOTICE = "Zitadel CLI collects anonymous usage analytics to help improve the tool. No personal data, project details, server URLs, or file contents are ever collected. Opt out any time with DO_NOT_TRACK=1, ZITADEL_TELEMETRY=0, or the --no-telemetry flag.";
1269
+ /**
1270
+ * Process-stable device facts shared by both the event bag and the user
1271
+ * profile, so the two never disagree on the same install's OS/arch/version.
1272
+ */
1273
+ function deviceFacts(meta) {
1274
+ return {
1275
+ osLabel: operatingSystem.value(process.platform),
1276
+ countryCode: country.value(void 0),
1277
+ platform: process.platform,
1278
+ arch: process.arch,
1279
+ nodeVersion: process.versions.node,
1280
+ cliVersion: meta.cliVersion
1281
+ };
1282
+ }
1283
+ function eventDeviceProperties(meta) {
1284
+ const facts = deviceFacts(meta);
1285
+ return {
1286
+ $os: facts.osLabel,
1287
+ mp_country_code: facts.countryCode,
1288
+ os: facts.platform,
1289
+ arch: facts.arch,
1290
+ node_version: facts.nodeVersion,
1291
+ cli_version: facts.cliVersion
1292
+ };
1293
+ }
1294
+ function profileDeviceProperties(meta) {
1295
+ const facts = deviceFacts(meta);
1296
+ return {
1297
+ $os: facts.osLabel,
1298
+ $country_code: facts.countryCode,
1299
+ os: facts.platform,
1300
+ arch: facts.arch,
1301
+ node_version: facts.nodeVersion,
1302
+ cli_version: facts.cliVersion
1303
+ };
1304
+ }
1305
+ /**
1306
+ * Build the dimensions shared by every lifecycle event, merged with any
1307
+ * per-command `extra`. The allow-list: only enums, booleans, counts, and
1308
+ * versions cross the boundary — never URLs, project ids, file paths, emails, or
1309
+ * secrets. `extra` is spread first so the canonical base dimensions always win;
1310
+ * callers order their own extras so reserved lifecycle fields win over command
1311
+ * props.
1312
+ */
1313
+ function commandEventProperties(meta, invocationId, extra = {}) {
1314
+ const { env } = meta;
1315
+ return {
1316
+ ...extra,
1317
+ ...eventDeviceProperties(meta),
1318
+ ip: 0,
1319
+ invocation_id: invocationId,
1320
+ command: meta.command,
1321
+ non_interactive: meta.nonInteractive,
1322
+ is_tty: meta.isTTY,
1323
+ is_ci: ciFlag.value(env),
1324
+ ci_provider: ciProvider.value(env),
1325
+ host_agent: hostAgent.value(env),
1326
+ invocation_channel: invocationChannel.value(env),
1327
+ dry_run: meta.dryRun,
1328
+ force: meta.force,
1329
+ server_kind: serverKind.value(meta.source)
1330
+ };
1331
+ }
1332
+ /**
1333
+ * Anonymous user-profile properties for this install, so the device appears
1334
+ * under Mixpanel "Users". Only non-PII device facts; `$name` is a readable,
1335
+ * non-identifying label that falls back to the OS when the host agent is
1336
+ * unknown. `$ip` is passed separately as a modifier by the caller.
1337
+ */
1338
+ function deviceProfileProperties(meta, distinctId) {
1339
+ const agent = hostAgent.value(meta.env);
1340
+ const label = agent === "unknown" ? process.platform : agent;
1341
+ return {
1342
+ ...profileDeviceProperties(meta),
1343
+ $name: `${label} · ${distinctId.slice(0, 8)}`,
1344
+ host_agent: agent
1345
+ };
1346
+ }
1347
+ //#endregion
1348
+ //#region src/lib/oclif/base.ts
1349
+ /**
1350
+ * Base class for every oclif command. Owns the global flags, builds the
1351
+ * {@link GlobalOptions} context (including server `source` resolution) the
1352
+ * subclass's `run` reads via `this.meta`, and turns the {@link CommandResult}
1353
+ * it returns into the JSON envelope (oclif serialises it natively in `--json`
1354
+ * mode) or human-facing text. Errors are translated into the failure envelope
1355
+ * and the mapped process exit code. Subclasses stay thin: parse flags, call
1356
+ * {@link toMeta}, do their work, and `return this.emit(...)`. The agent
1357
+ * contract (ADR 004) is preserved — oclif only replaces parsing, dispatch,
1358
+ * help, and JSON emission.
1359
+ */
1360
+ var BaseCommand = class extends Command {
1361
+ /** Opt into oclif's native `--json` flag and JSON serialisation of the result. */
1362
+ static enableJsonFlag = true;
1363
+ /** Flags shared by every command, inherited via oclif `baseFlags`. */
1364
+ static baseFlags = {
1365
+ cwd: Flags.string({
1366
+ char: "c",
1367
+ description: "Project directory to operate on."
1368
+ }),
1369
+ server: Flags.string({
1370
+ char: "s",
1371
+ description: "Override the resolved server URL."
1372
+ }),
1373
+ "non-interactive": Flags.boolean({
1374
+ char: "n",
1375
+ description: "Disable prompts. Required when scripting or running as an agent."
1376
+ }),
1377
+ force: Flags.boolean({
1378
+ char: "f",
1379
+ description: "Overwrite protected files on conflict."
1380
+ }),
1381
+ "dry-run": Flags.boolean({ description: "Preview without mutating files or the platform." }),
1382
+ verbose: Flags.boolean({ description: "Verbose logging." }),
1383
+ debug: Flags.boolean({ description: "Debug logging." }),
1384
+ telemetry: Flags.boolean({
1385
+ default: true,
1386
+ allowNo: true,
1387
+ description: "Send anonymous usage analytics. Disable with --no-telemetry."
1388
+ })
1389
+ };
1390
+ /** Resolved context for the current invocation; set by {@link toMeta}. */
1391
+ meta = this.fallbackMeta();
1392
+ /**
1393
+ * Anonymous usage analytics for this invocation, created once in
1394
+ * {@link toMeta}. Subclasses add command-specific dimensions (framework,
1395
+ * counts, `step`, …) via {@link recordTelemetry}; the base class
1396
+ * fires the lifecycle events and flushes in {@link finally}.
1397
+ */
1398
+ telemetry;
1399
+ /**
1400
+ * Per-command dimensions merged onto each lifecycle event emitted *after* they
1401
+ * are recorded — typically `completed`/`failed`, since `started` fires from
1402
+ * {@link openTelemetry} before a command body runs. Updated immutably via
1403
+ * {@link recordTelemetry} — never mutated in place.
1404
+ */
1405
+ telemetryProps = Object.freeze({});
1406
+ /** Correlates the started/completed pair; minted at instance construction. */
1407
+ telemetryInvocationId = randomUUID();
1408
+ /**
1409
+ * Wall-clock start used to derive `duration_ms`. Captured at instance
1410
+ * construction (before flag parsing and server resolution) so the duration
1411
+ * covers the full invocation, even when telemetry is opened late from
1412
+ * {@link catch} after an early failure.
1413
+ */
1414
+ telemetryStartedAt = Date.now();
1415
+ /**
1416
+ * Merge command-specific dimensions into {@link telemetryProps} immutably: a
1417
+ * new frozen bag replaces the previous one, so no shared object is ever
1418
+ * mutated. `step` advances by re-recording it at each milestone.
1419
+ */
1420
+ recordTelemetry(patch) {
1421
+ this.telemetryProps = Object.freeze({
1422
+ ...this.telemetryProps,
1423
+ ...patch
1424
+ });
1425
+ }
1426
+ /**
1427
+ * Builds {@link GlobalOptions} from parsed flags, resolving the server
1428
+ * `source` by the documented precedence and storing the result on
1429
+ * `this.meta` so the error handler can render a complete envelope.
1430
+ */
1431
+ async toMeta(flags, options = {}) {
1432
+ const cwd = resolveCwd(typeof flags.cwd === "string" ? flags.cwd : void 0);
1433
+ const serverFlag = typeof flags.server === "string" ? flags.server : void 0;
1434
+ const environment = typeof flags.environment === "string" ? flags.environment : "development";
1435
+ const source = options.resolveServer === false ? {
1436
+ value: options.source ?? "",
1437
+ origin: "default"
1438
+ } : await resolveServer({
1439
+ cwd,
1440
+ env: process.env,
1441
+ serverFlag,
1442
+ environment
1443
+ });
1444
+ const json = this.jsonEnabled();
1445
+ const isTTY = Boolean(process.stdout.isTTY && process.stdin.isTTY);
1446
+ const verbose = Boolean(flags.verbose);
1447
+ const debug = Boolean(flags.debug);
1448
+ consola.level = json ? -999 : debug ? 4 : 3;
1449
+ consola.options.formatOptions = {
1450
+ ...consola.options.formatOptions,
1451
+ date: false,
1452
+ colors: true,
1453
+ compact: true
1454
+ };
1455
+ this.meta = {
1456
+ cwd,
1457
+ nonInteractive: Boolean(flags["non-interactive"]) || !isTTY || json,
1458
+ dryRun: Boolean(flags["dry-run"]),
1459
+ force: Boolean(flags.force),
1460
+ command: this.id ?? "(default)",
1461
+ cliVersion: this.config.version,
1462
+ source: source.value,
1463
+ serverFlag,
1464
+ verbose,
1465
+ debug,
1466
+ env: process.env,
1467
+ isTTY
1468
+ };
1469
+ this.openTelemetry(typeof flags.telemetry === "boolean" ? flags.telemetry : void 0);
1470
+ return this.meta;
1471
+ }
1472
+ /**
1473
+ * Create telemetry once per invocation and open the lifecycle (started event +
1474
+ * anonymous profile + first-run notice), reading every dimension from the
1475
+ * current {@link meta}. Guarded so a command that resolves meta more than once
1476
+ * does not double-count. Also called from {@link catch} so a failure thrown
1477
+ * before {@link toMeta} finished (e.g. server resolution, flag parsing) still
1478
+ * records the run. Returns early for an inert (opted-out / no-token /
1479
+ * test-runner) instance so a disabled run never builds the property bags —
1480
+ * no timezone→country resolution or URL parsing for users who opted out. The
1481
+ * anonymous device profile is install-level and stable, so it is written only
1482
+ * on first run rather than paying a `people.set` request on every command.
1483
+ */
1484
+ /**
1485
+ * Telemetry factory seam. Production returns the real {@link Telemetry.create};
1486
+ * tests override it to inject a recording client and assert the lifecycle
1487
+ * ordering and opt-out behaviour that the central Vitest consent guard would
1488
+ * otherwise make untestable.
1489
+ */
1490
+ createTelemetry(deps) {
1491
+ return Telemetry.create(deps);
1492
+ }
1493
+ openTelemetry(flag) {
1494
+ if (this.telemetry) return;
1495
+ this.telemetry = this.createTelemetry({
1496
+ env: process.env,
1497
+ flag,
1498
+ debug: this.meta.debug
1499
+ });
1500
+ if (!this.telemetry.enabled) return;
1501
+ this.telemetry.track(CLI_COMMAND_STARTED, commandEventProperties(this.meta, this.telemetryInvocationId, this.telemetryProps));
1502
+ if (this.telemetry.isFirstRun) {
1503
+ this.telemetry.profile(deviceProfileProperties(this.meta, this.telemetry.distinctId), { $ip: 0 });
1504
+ if (this.isInteractive()) process.stderr.write(`${FIRST_RUN_NOTICE}\n`);
1505
+ }
1506
+ }
1507
+ /**
1508
+ * Whether this invocation is an interactive human session, used to gate the
1509
+ * one-time first-run notice. Derived from argv + `jsonEnabled()` + TTY rather
1510
+ * than `meta.nonInteractive`, so it is correct even on the early-failure path
1511
+ * where {@link catch} opens telemetry against a fallback meta that has not yet
1512
+ * computed `nonInteractive` from the flags.
1513
+ */
1514
+ isInteractive() {
1515
+ if (this.meta.nonInteractive || this.jsonEnabled()) return false;
1516
+ const argv = process.argv;
1517
+ if (argv.includes("--json") || argv.includes("--non-interactive") || argv.includes("-n")) return false;
1518
+ return Boolean(process.stdout.isTTY && process.stdin.isTTY);
1519
+ }
1520
+ /**
1521
+ * Final step of every command: in human mode it prints the rendered result
1522
+ * (oclif suppresses {@link Command.log} under `--json`); it returns the
1523
+ * envelope so oclif's `--json` path serialises it.
1524
+ */
1525
+ emit(result) {
1526
+ const normalized = normalizeCommandResult(result, this.meta);
1527
+ if (this.telemetry?.enabled) this.telemetry.track(CLI_COMMAND_COMPLETED, commandEventProperties(this.meta, this.telemetryInvocationId, {
1528
+ ...this.telemetryProps,
1529
+ status: result.status,
1530
+ duration_ms: Date.now() - this.telemetryStartedAt
1531
+ }));
1532
+ this.log(renderPretty(normalized, this.meta));
1533
+ return toEnvelope(normalized, this.meta);
1534
+ }
1535
+ /**
1536
+ * Renders any thrown error as the failure envelope and exits with its code.
1537
+ * A flag-parse error fires before {@link toMeta} runs, so the local `meta`
1538
+ * here refreshes `command` from the now-resolved command id to keep the
1539
+ * envelope's `command` field accurate. If that early failure left telemetry
1540
+ * unopened, {@link openTelemetry} runs here so the failure is still recorded;
1541
+ * the flag isn't parsed yet on that path, so `--no-telemetry` is honoured from
1542
+ * argv.
1543
+ */
1544
+ async catch(error) {
1545
+ const meta = {
1546
+ ...this.meta,
1547
+ command: this.id ?? this.meta.command
1548
+ };
1549
+ const zitadelError = toZitadelError(error);
1550
+ this.meta = meta;
1551
+ this.openTelemetry(process.argv.includes("--no-telemetry") ? false : void 0);
1552
+ if (this.telemetry?.enabled) this.telemetry.track(CLI_COMMAND_FAILED, commandEventProperties(meta, this.telemetryInvocationId, {
1553
+ ...this.telemetryProps,
1554
+ status: "error",
1555
+ reason: zitadelError.code,
1556
+ exit_code: zitadelError.exitCode,
1557
+ duration_ms: Date.now() - this.telemetryStartedAt
1558
+ }));
1559
+ if (this.jsonEnabled()) this.logJson(toErrorEnvelope(zitadelError, meta));
1560
+ else this.logToStderr(renderError(zitadelError, meta));
1561
+ return this.exit(zitadelError.exitCode);
1562
+ }
1563
+ /**
1564
+ * oclif runs this after `run`/`catch` on every path. We flush pending
1565
+ * telemetry so a short-lived CLI process does not exit before the lifecycle
1566
+ * event is sent. The await is bounded by the flush budget, so a hung or
1567
+ * firewalled network adds at most ~1s.
1568
+ *
1569
+ * `mixpanel@0.18` always uses keep-alive agents (hardcoded; not configurable)
1570
+ * and exposes no request timeout or handle, so a completed *or* hung request
1571
+ * leaves a socket that keeps Node's event loop open past the await. The
1572
+ * failure path force-exits via oclif's `exit()`, but the success path would
1573
+ * otherwise hang, so we arm an unref'd watchdog: it cannot keep the loop alive
1574
+ * on a clean exit, but if a telemetry socket is still holding it open after
1575
+ * the grace, it force-exits with the resolved code.
1576
+ */
1577
+ async finally(error) {
1578
+ await this.telemetry?.shutdown(1e3);
1579
+ if (this.telemetry?.enabled) setTimeout(() => process.exit(process.exitCode ?? 0), 250).unref();
1580
+ await super.finally(error);
1581
+ }
1582
+ /**
1583
+ * Context used before {@link toMeta} runs, so an error thrown during flag
1584
+ * parsing still renders a complete envelope. Version comes from oclif's
1585
+ * resolved {@link Command.config}.
1586
+ */
1587
+ fallbackMeta() {
1588
+ return {
1589
+ cwd: resolveCwd(void 0),
1590
+ nonInteractive: false,
1591
+ dryRun: false,
1592
+ force: false,
1593
+ command: "(default)",
1594
+ cliVersion: this.config.version,
1595
+ source: "",
1596
+ verbose: false,
1597
+ debug: false,
1598
+ env: process.env,
1599
+ isTTY: Boolean(process.stdout.isTTY && process.stdin.isTTY)
1600
+ };
1601
+ }
1602
+ };
1603
+ function normalizeCommandResult(result, meta) {
1604
+ if (result.status === "ok") return {
1605
+ ...result,
1606
+ data: normalizeDataNextCommands(result.data, meta)
1607
+ };
1608
+ return {
1609
+ ...result,
1610
+ data: normalizeDataNextCommands(result.data, meta),
1611
+ nextCommands: normalizePublicCliCommands(result.nextCommands, meta.cliVersion)
1612
+ };
1613
+ }
1614
+ function normalizeDataNextCommands(data, meta) {
1615
+ if (!isObject(data) || !Array.isArray(data.next_commands)) return data;
1616
+ return {
1617
+ ...data,
1618
+ next_commands: data.next_commands.map((command) => typeof command === "string" ? normalizePublicCliCommand(command, meta.cliVersion) : command)
1619
+ };
1620
+ }
1621
+ /** Wraps a {@link CommandResult} with the invocation metadata into the final envelope. */
1622
+ function toEnvelope(result, meta) {
1623
+ const base = {
1624
+ cli_version: meta.cliVersion,
1625
+ command: meta.command,
1626
+ source: meta.source
1627
+ };
1628
+ if (result.status === "ok") return {
1629
+ ...base,
1630
+ status: "ok",
1631
+ data: result.data,
1632
+ warnings: result.warnings ? [...result.warnings] : []
1633
+ };
1634
+ return {
1635
+ ...base,
1636
+ status: "skipped",
1637
+ reason: result.reason,
1638
+ data: result.data,
1639
+ next_commands: result.nextCommands ? [...result.nextCommands] : void 0
1640
+ };
1641
+ }
1642
+ /** Builds the failure envelope from a {@link ZitadelError} and the invocation metadata. */
1643
+ function toErrorEnvelope(error, meta) {
1644
+ return {
1645
+ status: "error",
1646
+ cli_version: meta.cliVersion,
1647
+ command: meta.command,
1648
+ source: meta.source,
1649
+ code: error.code,
1650
+ message: error.message,
1651
+ hint: error.hint,
1652
+ next_commands: normalizePublicCliCommands(error.nextCommands, meta.cliVersion),
1653
+ details: error.details
1654
+ };
1655
+ }
1656
+ /**
1657
+ * Renders a {@link CommandResult} as human-facing text for non-JSON mode. A
1658
+ * command may supply a bespoke `pretty` string (e.g. the `apply` plan diff);
1659
+ * otherwise success payloads are summarised by {@link formatData} and skips are
1660
+ * shown with their reason and follow-up commands.
1661
+ */
1662
+ function renderPretty(result, meta) {
1663
+ if (result.pretty !== void 0) return result.pretty;
1664
+ if (result.status === "ok") return formatData(result.data, result.warnings ? [...result.warnings] : [], meta);
1665
+ const lines = [`Skipped: ${result.reason}${suffixBlock(meta)}`];
1666
+ if (result.nextCommands && result.nextCommands.length > 0) {
1667
+ lines.push("Next:");
1668
+ for (const cmd of result.nextCommands) lines.push(` $ ${cmd}`);
1669
+ }
1670
+ return lines.join("\n");
1671
+ }
1672
+ /**
1673
+ * Renders a {@link ZitadelError} as a human-readable block for stderr: the
1674
+ * coded message, an optional hint, and any suggested next commands.
1675
+ */
1676
+ function renderError(error, meta) {
1677
+ const lines = [`Error ${error.code}: ${error.message}`];
1678
+ if (error.hint) lines.push(error.hint);
1679
+ const nextCommands = normalizePublicCliCommands(error.nextCommands, meta.cliVersion);
1680
+ if (nextCommands && nextCommands.length > 0) {
1681
+ lines.push("Next:");
1682
+ for (const cmd of nextCommands) lines.push(` $ ${cmd}`);
1683
+ }
1684
+ return lines.join("\n");
1685
+ }
1686
+ function formatData(data, warnings, opts) {
1687
+ if (typeof data === "string") {
1688
+ const suffix = sourceSuffix(opts);
1689
+ return suffix ? `${data}\n${suffix}` : data;
1690
+ }
1691
+ const lines = [];
1692
+ const titleLine = isObject(data) && typeof data.title === "string" ? String(data.title) : "Zitadel command completed.";
1693
+ lines.push(titleLine);
1694
+ const suffix = sourceSuffix(opts);
1695
+ if (suffix) lines.push(suffix);
1696
+ if (isObject(data)) {
1697
+ renderKnownSections(lines, data);
1698
+ if (Array.isArray(data.next_actions) && data.next_actions.length > 0) {
1699
+ lines.push("");
1700
+ lines.push("Next:");
1701
+ for (const action of data.next_actions) lines.push(` ${String(action)}`);
1702
+ }
1703
+ if (Array.isArray(data.next_commands) && data.next_commands.length > 0) {
1704
+ if (!Array.isArray(data.next_actions) || data.next_actions.length === 0) {
1705
+ lines.push("");
1706
+ lines.push("Next:");
1707
+ }
1708
+ for (const cmd of data.next_commands) lines.push(` $ ${String(cmd)}`);
1709
+ }
1710
+ }
1711
+ for (const warning of warnings.filter((warning) => !warningRenderedInChecks(data, warning))) lines.push(`Warning: ${warning}`);
1712
+ return lines.join("\n");
1713
+ }
1714
+ function warningRenderedInChecks(data, warning) {
1715
+ if (!isObject(data) || !Array.isArray(data.checks)) return false;
1716
+ return data.checks.some((check) => {
1717
+ if (!isObject(check) || check.status !== "warn") return false;
1718
+ return warning === `${String(check.name ?? "check")}: ${String(check.message ?? "")}`;
1719
+ });
1720
+ }
1721
+ function renderKnownSections(lines, data) {
1722
+ if (isObject(data.project)) {
1723
+ const project = data.project;
1724
+ const segments = [];
1725
+ if (typeof project.project_id === "string") segments.push(`project=${project.project_id}`);
1726
+ if (typeof project.lifecycle === "string") segments.push(`lifecycle=${project.lifecycle}`);
1727
+ if (typeof project.issuer === "string") segments.push(`issuer=${project.issuer}`);
1728
+ if (segments.length > 0) lines.push(`Project: ${segments.join(" ")}`);
1729
+ }
1730
+ if (typeof data.framework === "string") lines.push(`framework=${data.framework}`);
1731
+ if (Array.isArray(data.files_written) || Array.isArray(data.files_skipped)) {
1732
+ const written = Array.isArray(data.files_written) ? data.files_written.length : 0;
1733
+ const skippedCount = Array.isArray(data.files_skipped) ? data.files_skipped.length : 0;
1734
+ lines.push(`Files: ${written} written, ${skippedCount} unchanged`);
1735
+ }
1736
+ if (isObject(data.apply)) {
1737
+ const apply = data.apply;
1738
+ const bits = [];
1739
+ if (typeof apply.config_version === "number") bits.push(`v${apply.config_version}`);
1740
+ if (typeof apply.hash === "string") bits.push(`hash=${String(apply.hash).slice(0, 12)}`);
1741
+ if (typeof apply.environment === "string") bits.push(`env=${apply.environment}`);
1742
+ if (bits.length > 0) lines.push(`Apply: ${bits.join(" ")}`);
1743
+ }
1744
+ if (Array.isArray(data.checks) && data.checks.length > 0) {
1745
+ lines.push("Checks:");
1746
+ for (const check of data.checks) {
1747
+ if (!isObject(check)) continue;
1748
+ const status = check.status === "pass" ? "ok" : check.status === "warn" ? "warn" : "fail";
1749
+ lines.push(` [${status}] ${String(check.name ?? "check")}: ${String(check.message ?? "")}`);
1750
+ }
1751
+ }
1752
+ }
1753
+ function sourceSuffix(opts) {
1754
+ try {
1755
+ const url = new URL(opts.source);
1756
+ if (url.host === "api.zitadel.cloud") return "";
1757
+ return `(server: ${url.host})`;
1758
+ } catch {
1759
+ return "";
1760
+ }
1761
+ }
1762
+ function suffixBlock(opts) {
1763
+ const suffix = sourceSuffix(opts);
1764
+ return suffix ? ` ${suffix}` : "";
1765
+ }
1766
+ //#endregion
1767
+ export { npmDistTagForCliVersion as A, parseJsonObject as C, toZitadelError as D, ZitadelError as E, serverKind as M, normalizePublicCliJson as O, isObject as S, stableStringify as T, removeRuntimeMetadata as _, DEFAULT_LOCAL_SERVER_PORT as a, MANAGED_MARKER as b, checkLocalServerHealth as c, ensureContainerIdentity as d, ensureLocalState as f, removeLocalData as g, readRuntimeMetadata as h, CONTAINER_HTTP_PORT as i, publicCliCommand as j, normalizePublicCliProse as k, defaultLocalServerImageForCliVersion as l, localServerUrl as m, DEFAULT_SERVER as n, DEFAULT_LOCAL_SERVER_URL as o, localContainerName as p, CONTAINER_DATA_DIR as r, assertLocalStateWritable as s, BaseCommand as t, detectHealthyLocalServer as u, runtimeSummary as v, setTopLevelJsonKey as w, resolveCwd as x, writeRuntimeMetadata as y };
1768
+
1769
+ //# sourceMappingURL=oclif-CanO3zdt.mjs.map