@zitadel/cli 0.1.0-alpha.11 → 0.1.0-alpha.12

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 (34) hide show
  1. package/README.md +53 -35
  2. package/SKILLS.md +4 -0
  3. package/dist/commands/apply.mjs +9 -3
  4. package/dist/commands/apply.mjs.map +1 -1
  5. package/dist/commands/doctor.mjs +12 -5
  6. package/dist/commands/doctor.mjs.map +1 -1
  7. package/dist/commands/eject.mjs +3 -3
  8. package/dist/commands/logs.mjs +2 -2
  9. package/dist/commands/plan.mjs +9 -3
  10. package/dist/commands/plan.mjs.map +1 -1
  11. package/dist/commands/reset.mjs +2 -2
  12. package/dist/commands/setup.mjs +20 -3
  13. package/dist/commands/setup.mjs.map +1 -1
  14. package/dist/commands/start.mjs +4 -3
  15. package/dist/commands/start.mjs.map +1 -1
  16. package/dist/commands/status.mjs +3 -3
  17. package/dist/commands/stop.mjs +2 -2
  18. package/dist/{docker-CnGQK3ZK.mjs → docker-Djz6BLp9.mjs} +2 -2
  19. package/dist/{docker-CnGQK3ZK.mjs.map → docker-Djz6BLp9.mjs.map} +1 -1
  20. package/dist/{docker-guidance-ypN3IM3o.mjs → docker-guidance-D-33g1uV.mjs} +2 -2
  21. package/dist/{docker-guidance-ypN3IM3o.mjs.map → docker-guidance-D-33g1uV.mjs.map} +1 -1
  22. package/dist/{oclif-B7lBzh3R.mjs → oclif-BoUVygsZ.mjs} +708 -5
  23. package/dist/oclif-BoUVygsZ.mjs.map +1 -0
  24. package/dist/{orca-mzcHxDvu.mjs → orca-DU8myWGm.mjs} +59 -52
  25. package/dist/orca-DU8myWGm.mjs.map +1 -0
  26. package/dist/{project-Cd0L3PtM.mjs → project-B1qVQMKS.mjs} +2 -2
  27. package/dist/{project-Cd0L3PtM.mjs.map → project-B1qVQMKS.mjs.map} +1 -1
  28. package/dist/{sync-BojoQm2P.mjs → sync-CzruNz8K.mjs} +11 -5
  29. package/dist/sync-CzruNz8K.mjs.map +1 -0
  30. package/oclif.manifest.json +61 -1
  31. package/package.json +5 -4
  32. package/dist/oclif-B7lBzh3R.mjs.map +0 -1
  33. package/dist/orca-mzcHxDvu.mjs.map +0 -1
  34. package/dist/sync-BojoQm2P.mjs.map +0 -1
@@ -1,11 +1,14 @@
1
1
  import { Command, Flags } from "@oclif/core";
2
2
  import consola from "consola";
3
3
  import { ApiError } from "@zitadel/api/runtime/fetch";
4
+ import { createHash, randomUUID } from "node:crypto";
4
5
  import { stringify } from "safe-stable-stringify";
5
6
  import { dirname, join, resolve } from "node:path";
6
7
  import { access, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
7
- import { createHash } from "node:crypto";
8
- import { constants } from "node:fs";
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";
9
12
  //#region src/lib/errors.ts
10
13
  /**
11
14
  * Maps each {@link ZitadelErrorCode} to the process exit code the CLI
@@ -508,6 +511,582 @@ function readEnvServer(config, environment) {
508
511
  return typeof branch.server === "string" ? branch.server : void 0;
509
512
  }
510
513
  //#endregion
514
+ //#region src/lib/telemetry/config.ts
515
+ /**
516
+ * Resolves the Mixpanel ingestion token and API host for a CLI invocation.
517
+ *
518
+ * The token is a *write-only* project token: it can ingest events but cannot
519
+ * read data back, so — unlike the project service-key — it is safe to ship
520
+ * inside the published CLI. This mirrors how Next.js, Astro, and other dev
521
+ * tools embed their telemetry token, and is the only workable model for a CLI
522
+ * (we cannot ask end users to supply one). It is intentionally not a secret.
523
+ *
524
+ * Dev and prod are separate Mixpanel projects (the skill's Phase 2 rule: never
525
+ * track dev traffic into the production project). By default the channel comes
526
+ * from a build-time stamp (see {@link resolveChannel}), so the published CLI
527
+ * routes real user traffic to production without any per-user env var while
528
+ * source/test runs stay on dev — but a runtime `ZITADEL_TELEMETRY_ENV` or
529
+ * `ZITADEL_TELEMETRY_BUILD_CHANNEL` overrides the stamp, and
530
+ * `ZITADEL_TELEMETRY_TOKEN` overrides the token outright.
531
+ */
532
+ /**
533
+ * Development project token. Safe to commit (write-only ingestion key). Used
534
+ * when running from source or any non-production build.
535
+ */
536
+ const DEV_TELEMETRY_TOKEN = "0fb432b08a9797b87b0eebcbee11706e";
537
+ /**
538
+ * Production project token. Used by the published CLI (the build stamps the
539
+ * production channel) and any `ZITADEL_TELEMETRY_ENV=production` run. Write-only
540
+ * ingestion key, like the dev token — safe to commit.
541
+ */
542
+ const PROD_TELEMETRY_TOKEN = "f56fd7315ccd614fba8eecb2a8966152";
543
+ /** Mixpanel API hosts by data-residency region. */
544
+ const HOSTS = {
545
+ us: "api.mixpanel.com",
546
+ eu: "api-eu.mixpanel.com"
547
+ };
548
+ /**
549
+ * Channel stamped into the bundle at build time. tsdown's `define` always
550
+ * replaces the bare `__ZITADEL_TELEMETRY_CHANNEL__` identifier — with
551
+ * `"development"` by default and `"production"` only in the release build — so
552
+ * the shipped CLI routes to the right project with no per-user env var. The
553
+ * identifier is undefined only in unbundled runs (e.g. unit tests importing this
554
+ * module directly); the `typeof` guard returns `""` there so those runs fall
555
+ * through to the dev default without a ReferenceError.
556
+ */
557
+ function buildStampedChannel() {
558
+ return "development".trim().toLowerCase();
559
+ }
560
+ /**
561
+ * Decide which project the events belong to. Precedence: an explicit
562
+ * `ZITADEL_TELEMETRY_ENV`, then a `ZITADEL_TELEMETRY_BUILD_CHANNEL` env override
563
+ * (handy for CI/release), then the build-time channel stamp. The default —
564
+ * source/dev/test — is the dev project. Ambient `NODE_ENV` is deliberately NOT
565
+ * consulted: a source build with `NODE_ENV=production` must not route dev
566
+ * traffic to prod, nor a published run with `NODE_ENV=development` to dev.
567
+ */
568
+ function resolveChannel(env) {
569
+ const explicit = (env.ZITADEL_TELEMETRY_ENV ?? "").trim().toLowerCase();
570
+ if (explicit === "production") return "production";
571
+ if (explicit === "development") return "development";
572
+ return (env.ZITADEL_TELEMETRY_BUILD_CHANNEL ?? buildStampedChannel()).trim().toLowerCase() === "production" ? "production" : "development";
573
+ }
574
+ /**
575
+ * Resolve the ingestion token, or `undefined` when none is configured for the
576
+ * active channel. A `ZITADEL_TELEMETRY_TOKEN` override wins outright; otherwise
577
+ * the channel's baked token is used. An empty string (e.g. the unset prod
578
+ * token) resolves to `undefined`, which the caller treats as "telemetry off".
579
+ */
580
+ function resolveTelemetryToken(env) {
581
+ const override = env.ZITADEL_TELEMETRY_TOKEN?.trim();
582
+ if (override) return override;
583
+ const token = resolveChannel(env) === "production" ? PROD_TELEMETRY_TOKEN : DEV_TELEMETRY_TOKEN;
584
+ return token.length > 0 ? token : void 0;
585
+ }
586
+ /**
587
+ * Resolve the Mixpanel API host from `ZITADEL_TELEMETRY_REGION`. Defaults to the
588
+ * EU host, because the Zitadel Mixpanel projects live in the EU data-residency
589
+ * region. Set `ZITADEL_TELEMETRY_REGION=us` for a US-hosted project — events
590
+ * sent to the wrong host are silently dropped, so verify the first event lands
591
+ * in Live View.
592
+ */
593
+ function resolveTelemetryHost(env) {
594
+ return (env.ZITADEL_TELEMETRY_REGION ?? "eu").trim().toLowerCase() === "us" ? HOSTS.us : HOSTS.eu;
595
+ }
596
+ //#endregion
597
+ //#region src/lib/telemetry/consent.ts
598
+ const DISABLED_VALUES$1 = new Set([
599
+ "",
600
+ "0",
601
+ "false",
602
+ "off",
603
+ "no"
604
+ ]);
605
+ /**
606
+ * Resolve telemetry consent under an opt-out model: on by default, but any of
607
+ * several explicit signals turns it off, in precedence order.
608
+ *
609
+ * 1. `--no-telemetry` on the command line — the most explicit, per-invocation.
610
+ * 2. An automated test run (`VITEST`/`NODE_ENV=test`) — never emit synthetic
611
+ * traffic or pay the shutdown flush; spawned CLI subprocesses inherit it.
612
+ * 3. `DO_NOT_TRACK` — the cross-tool standard (https://consoledonottrack.com);
613
+ * any value other than `0`/empty disables.
614
+ * 4. `ZITADEL_TELEMETRY` set to a falsey token (`0`/`false`/`off`/`no`).
615
+ * 5. No ingestion token configured for the active channel — nothing to send to,
616
+ * so telemetry is inert regardless of consent.
617
+ *
618
+ * Consent being enabled does not by itself send anything; the caller still
619
+ * builds the client lazily and fails open on any transport error.
620
+ */
621
+ function resolveConsent(input) {
622
+ if (input.flag === false) return {
623
+ enabled: false,
624
+ reason: "flag-opt-out"
625
+ };
626
+ if (input.env.VITEST || input.env.NODE_ENV === "test") return {
627
+ enabled: false,
628
+ reason: "test-runner"
629
+ };
630
+ const doNotTrack = input.env.DO_NOT_TRACK?.trim();
631
+ if (doNotTrack && doNotTrack !== "0") return {
632
+ enabled: false,
633
+ reason: "do-not-track"
634
+ };
635
+ const explicit = input.env.ZITADEL_TELEMETRY?.trim().toLowerCase();
636
+ if (explicit !== void 0 && DISABLED_VALUES$1.has(explicit)) return {
637
+ enabled: false,
638
+ reason: "env-opt-out"
639
+ };
640
+ const token = resolveTelemetryToken(input.env);
641
+ if (!token) return {
642
+ enabled: false,
643
+ reason: "no-token"
644
+ };
645
+ return {
646
+ enabled: true,
647
+ reason: "enabled",
648
+ token
649
+ };
650
+ }
651
+ //#endregion
652
+ //#region src/lib/telemetry/identity.ts
653
+ /**
654
+ * Resolve the directory the CLI persists cross-invocation state in, honoring
655
+ * the platform conventions: `XDG_CONFIG_HOME` then `~/.config` on Unix, and
656
+ * `%APPDATA%` on Windows. The anonymous id lives here so it survives between
657
+ * runs without touching the user's project tree.
658
+ */
659
+ function telemetryConfigDir(env) {
660
+ if (process.platform === "win32" && env.APPDATA) return join(env.APPDATA, "zitadel", "cli");
661
+ return join(env.XDG_CONFIG_HOME?.trim() || join(homedir(), ".config"), "zitadel", "cli");
662
+ }
663
+ /**
664
+ * Load the persisted anonymous id, minting and storing a new one on first run.
665
+ *
666
+ * Fail-open: if the config dir cannot be resolved, read, or written — including
667
+ * an `os.homedir()` throw in a restricted/containerized environment, a
668
+ * read-only home, a CI sandbox, or a permissions error — fall back to an
669
+ * ephemeral per-process id and report `isFirstRun: false` so we neither crash
670
+ * nor nag the user with the notice on every run. Telemetry is best-effort,
671
+ * never load-bearing.
672
+ */
673
+ function loadOrCreateIdentity(env) {
674
+ let dir;
675
+ try {
676
+ dir = telemetryConfigDir(env);
677
+ } catch {
678
+ return {
679
+ distinctId: randomUUID(),
680
+ isFirstRun: false
681
+ };
682
+ }
683
+ const file = join(dir, "telemetry.json");
684
+ try {
685
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
686
+ if (typeof parsed.distinctId === "string" && parsed.distinctId.length > 0) return {
687
+ distinctId: parsed.distinctId,
688
+ isFirstRun: false
689
+ };
690
+ } catch {}
691
+ const distinctId = randomUUID();
692
+ try {
693
+ mkdirSync(dir, { recursive: true });
694
+ writeFileSync(file, `${JSON.stringify({ distinctId })}\n`, { mode: 384 });
695
+ return {
696
+ distinctId,
697
+ isFirstRun: true
698
+ };
699
+ } catch {
700
+ return {
701
+ distinctId,
702
+ isFirstRun: false
703
+ };
704
+ }
705
+ }
706
+ //#endregion
707
+ //#region src/lib/telemetry/util.ts
708
+ /**
709
+ * Return a new bag with empty values stripped, per Mixpanel's "omit, never send
710
+ * null/''" rule. Pure — the input is never mutated.
711
+ */
712
+ function compact(properties) {
713
+ return Object.fromEntries(Object.entries(properties).filter(([, value]) => value !== void 0 && value !== null && value !== ""));
714
+ }
715
+ //#endregion
716
+ //#region src/lib/telemetry/index.ts
717
+ /**
718
+ * A generic, application-agnostic Mixpanel client wrapper for a single short
719
+ * process. It knows nothing about commands, CLIs, or event names — callers
720
+ * supply fully-built property bags. Its only opinions are operational, because
721
+ * telemetry must never degrade the host program:
722
+ *
723
+ * - Never throws — a missing token, opt-out, or transport error all degrade to
724
+ * a silent no-op.
725
+ * - Never blocks beyond {@link shutdown}'s timeout.
726
+ * - Never writes to stdout; the optional debug trace goes to stderr.
727
+ *
728
+ * Inputs are treated as immutable: {@link track}/{@link profile} build a new
729
+ * payload via spread and never mutate the bag they are given.
730
+ */
731
+ var Telemetry = class Telemetry {
732
+ pending = [];
733
+ constructor(client, distinctId, isFirstRun, debug, newId) {
734
+ this.client = client;
735
+ this.distinctId = distinctId;
736
+ this.isFirstRun = isFirstRun;
737
+ this.debug = debug;
738
+ this.newId = newId;
739
+ }
740
+ /** Whether events will actually be sent (consent granted and a token configured). */
741
+ get enabled() {
742
+ return this.client !== void 0;
743
+ }
744
+ /**
745
+ * Resolve consent, identity, token, and host, then build the Mixpanel client
746
+ * lazily — only when consent is granted *and* a token is configured. Any
747
+ * failure along the way yields an inert instance whose methods are no-ops.
748
+ */
749
+ static create(deps) {
750
+ const newId = deps.newId ?? randomUUID;
751
+ const debug = deps.debug ?? false;
752
+ const inert = (firstRun) => new Telemetry(void 0, "", firstRun, debug, newId);
753
+ const consent = resolveConsent({
754
+ env: deps.env,
755
+ flag: deps.flag
756
+ });
757
+ if (debug) process.stderr.write(`[telemetry] consent: ${consent.reason}\n`);
758
+ if (!consent.enabled || !consent.token) return inert(false);
759
+ const identity = (deps.loadIdentity ?? loadOrCreateIdentity)(deps.env);
760
+ let client;
761
+ try {
762
+ client = (deps.initClient ?? defaultInit)(consent.token, resolveTelemetryHost(deps.env));
763
+ } catch {
764
+ return inert(identity.isFirstRun);
765
+ }
766
+ return new Telemetry(client, identity.distinctId, identity.isFirstRun, debug, newId);
767
+ }
768
+ /** Send an event with the given properties; `distinct_id`/`$insert_id` are stamped here. */
769
+ track(event, properties) {
770
+ if (!this.client) return;
771
+ const payload = compact({
772
+ ...properties,
773
+ distinct_id: this.distinctId,
774
+ $insert_id: this.newId()
775
+ });
776
+ if (this.debug) process.stderr.write(`[telemetry] ${event} ${JSON.stringify(payload)}\n`);
777
+ this.enqueue((client, done) => client.track(event, payload, done));
778
+ }
779
+ /** Write a user profile via the People API. `modifiers` carries `$ip` etc. */
780
+ profile(properties, modifiers = {}) {
781
+ if (!this.client) return;
782
+ const payload = compact(properties);
783
+ if (this.debug) process.stderr.write(`[telemetry] people.set ${JSON.stringify(payload)}\n`);
784
+ this.enqueue((client, done) => client.people.set(this.distinctId, payload, modifiers, done));
785
+ }
786
+ /**
787
+ * Await in-flight sends so a short-lived process does not exit before the
788
+ * requests complete, bounded by `timeoutMs`. The timeout is unref'd so the
789
+ * losing race branch never holds the event loop open or needs a manual clear.
790
+ * Safe to call on an inert instance.
791
+ */
792
+ async shutdown(timeoutMs = 2e3) {
793
+ if (this.pending.length === 0) return;
794
+ await Promise.race([Promise.allSettled(this.pending), setTimeout$1(timeoutMs, void 0, { ref: false })]);
795
+ }
796
+ /**
797
+ * Enqueue one fire-and-forget send. Resolves (never rejects) on any failure —
798
+ * a synchronous throw or an error callback must not surface to the caller or
799
+ * leak as an unhandled rejection.
800
+ */
801
+ enqueue(send) {
802
+ const client = this.client;
803
+ if (!client) return;
804
+ this.pending.push(new Promise((resolve) => {
805
+ try {
806
+ send(client, () => resolve());
807
+ } catch {
808
+ resolve();
809
+ }
810
+ }));
811
+ }
812
+ };
813
+ /** Real Mixpanel client construction, isolated so {@link Telemetry.create} can swap it in tests. */
814
+ function defaultInit(token, host) {
815
+ return mixpanelLib.init(token, {
816
+ host,
817
+ geolocate: false
818
+ });
819
+ }
820
+ //#endregion
821
+ //#region src/lib/telemetry/dimensions/env-flag.ts
822
+ const DISABLED_VALUES = new Set([
823
+ "",
824
+ "0",
825
+ "false",
826
+ "off",
827
+ "no"
828
+ ]);
829
+ /**
830
+ * Whether an environment variable is set to an enabled value. A present-but-
831
+ * falsey string (`CI=false`, `CI=0`) counts as disabled — unlike a bare
832
+ * `Boolean(env.CI)` check, which is true for any non-empty string.
833
+ */
834
+ function envEnabled(value) {
835
+ return value !== void 0 && !DISABLED_VALUES.has(value.trim().toLowerCase());
836
+ }
837
+ //#endregion
838
+ //#region src/lib/telemetry/dimensions/ci-flag.ts
839
+ /** Whether the process is running inside an automated CI environment. */
840
+ var CiFlag = class {
841
+ value(env) {
842
+ return envEnabled(env.CI) || envEnabled(env.GITHUB_ACTIONS) || envEnabled(env.GITLAB_CI);
843
+ }
844
+ };
845
+ const ciFlag = new CiFlag();
846
+ const ciProvider = new class CiProvider {
847
+ /** Provider-marker env var → reported name, in match order. */
848
+ static providers = [
849
+ ["GITHUB_ACTIONS", "github_actions"],
850
+ ["GITLAB_CI", "gitlab_ci"],
851
+ ["CIRCLECI", "circleci"],
852
+ ["BUILDKITE", "buildkite"],
853
+ ["JENKINS_URL", "jenkins"]
854
+ ];
855
+ value(env) {
856
+ const named = CiProvider.providers.find(([marker]) => envEnabled(env[marker]));
857
+ if (named) return named[1];
858
+ return envEnabled(env.CI) ? "unknown" : void 0;
859
+ }
860
+ }();
861
+ //#endregion
862
+ //#region src/lib/telemetry/dimensions/country.ts
863
+ /**
864
+ * ISO 3166-1 alpha-2 country for an IANA timezone, defaulting to the machine's
865
+ * own zone. A curated subset of common zones — any zone not listed (or an
866
+ * unknown/unavailable one) yields `undefined`, since we report no country
867
+ * rather than guess. Derived from the timezone, never the IP, so no city or
868
+ * region is ever inferred. The machine's own zone is resolved once and cached:
869
+ * `Intl.DateTimeFormat` construction loads ICU data and is process-stable.
870
+ */
871
+ var Country = class {
872
+ byTimezone = {
873
+ "Africa/Abidjan": "CI",
874
+ "Africa/Accra": "GH",
875
+ "Africa/Addis_Ababa": "ET",
876
+ "Africa/Algiers": "DZ",
877
+ "Africa/Cairo": "EG",
878
+ "Africa/Casablanca": "MA",
879
+ "Africa/Johannesburg": "ZA",
880
+ "Africa/Lagos": "NG",
881
+ "Africa/Nairobi": "KE",
882
+ "Africa/Tunis": "TN",
883
+ "America/Anchorage": "US",
884
+ "America/Argentina/Buenos_Aires": "AR",
885
+ "America/Bogota": "CO",
886
+ "America/Chicago": "US",
887
+ "America/Denver": "US",
888
+ "America/Halifax": "CA",
889
+ "America/Lima": "PE",
890
+ "America/Los_Angeles": "US",
891
+ "America/Mexico_City": "MX",
892
+ "America/New_York": "US",
893
+ "America/Phoenix": "US",
894
+ "America/Santiago": "CL",
895
+ "America/Sao_Paulo": "BR",
896
+ "America/Toronto": "CA",
897
+ "America/Vancouver": "CA",
898
+ "Asia/Bangkok": "TH",
899
+ "Asia/Dhaka": "BD",
900
+ "Asia/Dubai": "AE",
901
+ "Asia/Hong_Kong": "HK",
902
+ "Asia/Jakarta": "ID",
903
+ "Asia/Jerusalem": "IL",
904
+ "Asia/Karachi": "PK",
905
+ "Asia/Kolkata": "IN",
906
+ "Asia/Kuala_Lumpur": "MY",
907
+ "Asia/Manila": "PH",
908
+ "Asia/Riyadh": "SA",
909
+ "Asia/Seoul": "KR",
910
+ "Asia/Shanghai": "CN",
911
+ "Asia/Singapore": "SG",
912
+ "Asia/Taipei": "TW",
913
+ "Asia/Tehran": "IR",
914
+ "Asia/Tokyo": "JP",
915
+ "Australia/Adelaide": "AU",
916
+ "Australia/Brisbane": "AU",
917
+ "Australia/Melbourne": "AU",
918
+ "Australia/Perth": "AU",
919
+ "Australia/Sydney": "AU",
920
+ "Europe/Amsterdam": "NL",
921
+ "Europe/Athens": "GR",
922
+ "Europe/Berlin": "DE",
923
+ "Europe/Brussels": "BE",
924
+ "Europe/Bucharest": "RO",
925
+ "Europe/Budapest": "HU",
926
+ "Europe/Copenhagen": "DK",
927
+ "Europe/Dublin": "IE",
928
+ "Europe/Helsinki": "FI",
929
+ "Europe/Istanbul": "TR",
930
+ "Europe/Kyiv": "UA",
931
+ "Europe/Lisbon": "PT",
932
+ "Europe/London": "GB",
933
+ "Europe/Madrid": "ES",
934
+ "Europe/Moscow": "RU",
935
+ "Europe/Oslo": "NO",
936
+ "Europe/Paris": "FR",
937
+ "Europe/Prague": "CZ",
938
+ "Europe/Rome": "IT",
939
+ "Europe/Stockholm": "SE",
940
+ "Europe/Vienna": "AT",
941
+ "Europe/Warsaw": "PL",
942
+ "Europe/Zurich": "CH",
943
+ "Pacific/Auckland": "NZ",
944
+ "Pacific/Honolulu": "US"
945
+ };
946
+ machineZone;
947
+ machineZoneResolved = false;
948
+ value(timezone) {
949
+ const zone = timezone ?? this.resolveMachineZone();
950
+ return zone ? this.byTimezone[zone] : void 0;
951
+ }
952
+ resolveMachineZone() {
953
+ if (!this.machineZoneResolved) {
954
+ this.machineZoneResolved = true;
955
+ try {
956
+ this.machineZone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
957
+ } catch {
958
+ this.machineZone = void 0;
959
+ }
960
+ }
961
+ return this.machineZone;
962
+ }
963
+ };
964
+ const country = new Country();
965
+ const hostAgent = new class HostAgent {
966
+ /** Predicate → reported name, in match order. */
967
+ static agents = [
968
+ [(env) => Boolean(env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT), "claude_code"],
969
+ [(env) => Boolean(env.CURSOR_TRACE_ID || env.CURSOR_AGENT), "cursor"],
970
+ [(env) => env.TERM_PROGRAM === "vscode", "vscode"]
971
+ ];
972
+ value(env) {
973
+ return HostAgent.agents.find(([matches]) => matches(env))?.[1] ?? "unknown";
974
+ }
975
+ }();
976
+ const invocationChannel = new class InvocationChannel {
977
+ /** Ordered so a more specific name wins — `pnpm` is matched before the `npm` prefix. */
978
+ static managers = [
979
+ "pnpm",
980
+ "yarn",
981
+ "bun",
982
+ "npm"
983
+ ];
984
+ value(env) {
985
+ const userAgent = env.npm_config_user_agent ?? "";
986
+ return InvocationChannel.managers.find((manager) => userAgent.startsWith(manager)) ?? "unknown";
987
+ }
988
+ }();
989
+ const operatingSystem = new class OperatingSystem {
990
+ static labels = {
991
+ darwin: "Mac OS X",
992
+ win32: "Windows",
993
+ linux: "Linux",
994
+ freebsd: "BSD",
995
+ openbsd: "BSD",
996
+ netbsd: "BSD",
997
+ aix: "AIX",
998
+ sunos: "Solaris"
999
+ };
1000
+ value(platform) {
1001
+ return OperatingSystem.labels[platform] ?? platform;
1002
+ }
1003
+ }();
1004
+ //#endregion
1005
+ //#region src/lib/oclif/server-kind.ts
1006
+ /**
1007
+ * Buckets the resolved backend `source` into a coarse kind. The raw URL is never
1008
+ * emitted — it can carry an internal/self-hosted hostname — only which kind of
1009
+ * backend the command targeted.
1010
+ */
1011
+ var ServerKind = class {
1012
+ value(source) {
1013
+ if (source === "mock") return "local";
1014
+ if (!URL.canParse(source)) return "unknown";
1015
+ const { hostname } = new URL(source);
1016
+ if (hostname === "zitadel.cloud" || hostname.endsWith(".zitadel.cloud")) return "cloud";
1017
+ if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1") return "local";
1018
+ return "self_hosted";
1019
+ }
1020
+ };
1021
+ const serverKind = new ServerKind();
1022
+ //#endregion
1023
+ //#region src/lib/oclif/command-telemetry.ts
1024
+ /**
1025
+ * CLI-specific telemetry glue: the only place that turns a {@link GlobalOptions}
1026
+ * invocation into the property bags the generic `Telemetry` client sends,
1027
+ * keeping `lib/telemetry` free of CLI coupling.
1028
+ */
1029
+ const CLI_COMMAND_STARTED = "cli_command_started";
1030
+ const CLI_COMMAND_COMPLETED = "cli_command_completed";
1031
+ const CLI_COMMAND_FAILED = "cli_command_failed";
1032
+ 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.";
1033
+ /**
1034
+ * Process-stable device facts shared by both the event bag and the user
1035
+ * profile, so the two never disagree on the same install's OS/arch/version.
1036
+ */
1037
+ function deviceProperties(meta) {
1038
+ return {
1039
+ $os: operatingSystem.value(process.platform),
1040
+ $country_code: country.value(void 0),
1041
+ os: process.platform,
1042
+ arch: process.arch,
1043
+ node_version: process.versions.node,
1044
+ cli_version: meta.cliVersion
1045
+ };
1046
+ }
1047
+ /**
1048
+ * Build the dimensions shared by every lifecycle event, merged with any
1049
+ * per-command `extra`. The allow-list: only enums, booleans, counts, and
1050
+ * versions cross the boundary — never URLs, project ids, file paths, emails, or
1051
+ * secrets. `extra` is spread first so the canonical base dimensions always win;
1052
+ * callers order their own extras so reserved lifecycle fields win over command
1053
+ * props.
1054
+ */
1055
+ function commandEventProperties(meta, invocationId, extra = {}) {
1056
+ const { env } = meta;
1057
+ return {
1058
+ ...extra,
1059
+ ...deviceProperties(meta),
1060
+ ip: 0,
1061
+ invocation_id: invocationId,
1062
+ command: meta.command,
1063
+ non_interactive: meta.nonInteractive,
1064
+ is_tty: meta.isTTY,
1065
+ is_ci: ciFlag.value(env),
1066
+ ci_provider: ciProvider.value(env),
1067
+ host_agent: hostAgent.value(env),
1068
+ invocation_channel: invocationChannel.value(env),
1069
+ dry_run: meta.dryRun,
1070
+ force: meta.force,
1071
+ server_kind: serverKind.value(meta.source)
1072
+ };
1073
+ }
1074
+ /**
1075
+ * Anonymous user-profile properties for this install, so the device appears
1076
+ * under Mixpanel "Users". Only non-PII device facts; `$name` is a readable,
1077
+ * non-identifying label that falls back to the OS when the host agent is
1078
+ * unknown. `$ip` is passed separately as a modifier by the caller.
1079
+ */
1080
+ function deviceProfileProperties(meta, distinctId) {
1081
+ const agent = hostAgent.value(meta.env);
1082
+ const label = agent === "unknown" ? process.platform : agent;
1083
+ return {
1084
+ ...deviceProperties(meta),
1085
+ $name: `${label} · ${distinctId.slice(0, 8)}`,
1086
+ host_agent: agent
1087
+ };
1088
+ }
1089
+ //#endregion
511
1090
  //#region src/lib/oclif/base.ts
512
1091
  /**
513
1092
  * Base class for every oclif command. Owns the global flags, builds the
@@ -543,11 +1122,50 @@ var BaseCommand = class extends Command {
543
1122
  }),
544
1123
  "dry-run": Flags.boolean({ description: "Preview without mutating files or the platform." }),
545
1124
  verbose: Flags.boolean({ description: "Verbose logging." }),
546
- debug: Flags.boolean({ description: "Debug logging." })
1125
+ debug: Flags.boolean({ description: "Debug logging." }),
1126
+ telemetry: Flags.boolean({
1127
+ default: true,
1128
+ allowNo: true,
1129
+ description: "Send anonymous usage analytics. Disable with --no-telemetry."
1130
+ })
547
1131
  };
548
1132
  /** Resolved context for the current invocation; set by {@link toMeta}. */
549
1133
  meta = this.fallbackMeta();
550
1134
  /**
1135
+ * Anonymous usage analytics for this invocation, created once in
1136
+ * {@link toMeta}. Subclasses add command-specific dimensions (framework,
1137
+ * counts, `step`, …) via {@link recordTelemetry}; the base class
1138
+ * fires the lifecycle events and flushes in {@link finally}.
1139
+ */
1140
+ telemetry;
1141
+ /**
1142
+ * Per-command dimensions merged onto each lifecycle event emitted *after* they
1143
+ * are recorded — typically `completed`/`failed`, since `started` fires from
1144
+ * {@link openTelemetry} before a command body runs. Updated immutably via
1145
+ * {@link recordTelemetry} — never mutated in place.
1146
+ */
1147
+ telemetryProps = Object.freeze({});
1148
+ /** Correlates the started/completed pair; minted at instance construction. */
1149
+ telemetryInvocationId = randomUUID();
1150
+ /**
1151
+ * Wall-clock start used to derive `duration_ms`. Captured at instance
1152
+ * construction (before flag parsing and server resolution) so the duration
1153
+ * covers the full invocation, even when telemetry is opened late from
1154
+ * {@link catch} after an early failure.
1155
+ */
1156
+ telemetryStartedAt = Date.now();
1157
+ /**
1158
+ * Merge command-specific dimensions into {@link telemetryProps} immutably: a
1159
+ * new frozen bag replaces the previous one, so no shared object is ever
1160
+ * mutated. `step` advances by re-recording it at each milestone.
1161
+ */
1162
+ recordTelemetry(patch) {
1163
+ this.telemetryProps = Object.freeze({
1164
+ ...this.telemetryProps,
1165
+ ...patch
1166
+ });
1167
+ }
1168
+ /**
551
1169
  * Builds {@link GlobalOptions} from parsed flags, resolving the server
552
1170
  * `source` by the documented precedence and storing the result on
553
1171
  * `this.meta` so the error handler can render a complete envelope.
@@ -590,15 +1208,69 @@ var BaseCommand = class extends Command {
590
1208
  env: process.env,
591
1209
  isTTY
592
1210
  };
1211
+ this.openTelemetry(typeof flags.telemetry === "boolean" ? flags.telemetry : void 0);
593
1212
  return this.meta;
594
1213
  }
595
1214
  /**
1215
+ * Create telemetry once per invocation and open the lifecycle (started event +
1216
+ * anonymous profile + first-run notice), reading every dimension from the
1217
+ * current {@link meta}. Guarded so a command that resolves meta more than once
1218
+ * does not double-count. Also called from {@link catch} so a failure thrown
1219
+ * before {@link toMeta} finished (e.g. server resolution, flag parsing) still
1220
+ * records the run. Returns early for an inert (opted-out / no-token /
1221
+ * test-runner) instance so a disabled run never builds the property bags —
1222
+ * no timezone→country resolution or URL parsing for users who opted out. The
1223
+ * anonymous device profile is install-level and stable, so it is written only
1224
+ * on first run rather than paying a `people.set` request on every command.
1225
+ */
1226
+ /**
1227
+ * Telemetry factory seam. Production returns the real {@link Telemetry.create};
1228
+ * tests override it to inject a recording client and assert the lifecycle
1229
+ * ordering and opt-out behaviour that the central Vitest consent guard would
1230
+ * otherwise make untestable.
1231
+ */
1232
+ createTelemetry(deps) {
1233
+ return Telemetry.create(deps);
1234
+ }
1235
+ openTelemetry(flag) {
1236
+ if (this.telemetry) return;
1237
+ this.telemetry = this.createTelemetry({
1238
+ env: process.env,
1239
+ flag,
1240
+ debug: this.meta.debug
1241
+ });
1242
+ if (!this.telemetry.enabled) return;
1243
+ this.telemetry.track(CLI_COMMAND_STARTED, commandEventProperties(this.meta, this.telemetryInvocationId, this.telemetryProps));
1244
+ if (this.telemetry.isFirstRun) {
1245
+ this.telemetry.profile(deviceProfileProperties(this.meta, this.telemetry.distinctId), { $ip: 0 });
1246
+ if (this.isInteractive()) process.stderr.write(`${FIRST_RUN_NOTICE}\n`);
1247
+ }
1248
+ }
1249
+ /**
1250
+ * Whether this invocation is an interactive human session, used to gate the
1251
+ * one-time first-run notice. Derived from argv + `jsonEnabled()` + TTY rather
1252
+ * than `meta.nonInteractive`, so it is correct even on the early-failure path
1253
+ * where {@link catch} opens telemetry against a fallback meta that has not yet
1254
+ * computed `nonInteractive` from the flags.
1255
+ */
1256
+ isInteractive() {
1257
+ if (this.meta.nonInteractive || this.jsonEnabled()) return false;
1258
+ const argv = process.argv;
1259
+ if (argv.includes("--json") || argv.includes("--non-interactive") || argv.includes("-n")) return false;
1260
+ return Boolean(process.stdout.isTTY && process.stdin.isTTY);
1261
+ }
1262
+ /**
596
1263
  * Final step of every command: in human mode it prints the rendered result
597
1264
  * (oclif suppresses {@link Command.log} under `--json`); it returns the
598
1265
  * envelope so oclif's `--json` path serialises it.
599
1266
  */
600
1267
  emit(result) {
601
1268
  const normalized = normalizeCommandResult(result, this.meta);
1269
+ if (this.telemetry?.enabled) this.telemetry.track(CLI_COMMAND_COMPLETED, commandEventProperties(this.meta, this.telemetryInvocationId, {
1270
+ ...this.telemetryProps,
1271
+ status: result.status,
1272
+ duration_ms: Date.now() - this.telemetryStartedAt
1273
+ }));
602
1274
  this.log(renderPretty(normalized, this.meta));
603
1275
  return toEnvelope(normalized, this.meta);
604
1276
  }
@@ -606,7 +1278,10 @@ var BaseCommand = class extends Command {
606
1278
  * Renders any thrown error as the failure envelope and exits with its code.
607
1279
  * A flag-parse error fires before {@link toMeta} runs, so the local `meta`
608
1280
  * here refreshes `command` from the now-resolved command id to keep the
609
- * envelope's `command` field accurate.
1281
+ * envelope's `command` field accurate. If that early failure left telemetry
1282
+ * unopened, {@link openTelemetry} runs here so the failure is still recorded;
1283
+ * the flag isn't parsed yet on that path, so `--no-telemetry` is honoured from
1284
+ * argv.
610
1285
  */
611
1286
  async catch(error) {
612
1287
  const meta = {
@@ -614,11 +1289,39 @@ var BaseCommand = class extends Command {
614
1289
  command: this.id ?? this.meta.command
615
1290
  };
616
1291
  const zitadelError = toZitadelError(error);
1292
+ this.meta = meta;
1293
+ this.openTelemetry(process.argv.includes("--no-telemetry") ? false : void 0);
1294
+ if (this.telemetry?.enabled) this.telemetry.track(CLI_COMMAND_FAILED, commandEventProperties(meta, this.telemetryInvocationId, {
1295
+ ...this.telemetryProps,
1296
+ status: "error",
1297
+ reason: zitadelError.code,
1298
+ exit_code: zitadelError.exitCode,
1299
+ duration_ms: Date.now() - this.telemetryStartedAt
1300
+ }));
617
1301
  if (this.jsonEnabled()) this.logJson(toErrorEnvelope(zitadelError, meta));
618
1302
  else this.logToStderr(renderError(zitadelError, meta));
619
1303
  return this.exit(zitadelError.exitCode);
620
1304
  }
621
1305
  /**
1306
+ * oclif runs this after `run`/`catch` on every path. We flush pending
1307
+ * telemetry so a short-lived CLI process does not exit before the lifecycle
1308
+ * event is sent. The await is bounded by the flush budget, so a hung or
1309
+ * firewalled network adds at most ~1s.
1310
+ *
1311
+ * `mixpanel@0.18` always uses keep-alive agents (hardcoded; not configurable)
1312
+ * and exposes no request timeout or handle, so a completed *or* hung request
1313
+ * leaves a socket that keeps Node's event loop open past the await. The
1314
+ * failure path force-exits via oclif's `exit()`, but the success path would
1315
+ * otherwise hang, so we arm an unref'd watchdog: it cannot keep the loop alive
1316
+ * on a clean exit, but if a telemetry socket is still holding it open after
1317
+ * the grace, it force-exits with the resolved code.
1318
+ */
1319
+ async finally(error) {
1320
+ await this.telemetry?.shutdown(1e3);
1321
+ if (this.telemetry?.enabled) setTimeout(() => process.exit(process.exitCode ?? 0), 250).unref();
1322
+ await super.finally(error);
1323
+ }
1324
+ /**
622
1325
  * Context used before {@link toMeta} runs, so an error thrown during flag
623
1326
  * parsing still renders a complete envelope. Version comes from oclif's
624
1327
  * resolved {@link Command.config}.
@@ -805,4 +1508,4 @@ function suffixBlock(opts) {
805
1508
  //#endregion
806
1509
  export { isObject as C, toZitadelError as D, ZitadelError as E, resolveCwd as S, stableStringify as T, runtimeSummary as _, DEFAULT_LOCAL_SERVER_PORT as a, publicCliCommand as b, checkLocalServerHealth as c, ensureLocalState as d, localContainerName as f, removeRuntimeMetadata as g, removeLocalData as h, CONTAINER_HTTP_PORT as i, defaultLocalServerImageForCliVersion as l, readRuntimeMetadata as m, DEFAULT_SERVER as n, DEFAULT_LOCAL_SERVER_URL as o, localServerUrl as p, CONTAINER_DATA_DIR as r, assertLocalStateWritable as s, BaseCommand as t, ensureContainerIdentity as u, writeRuntimeMetadata as v, parseJsonObject as w, MANAGED_MARKER as x, npmDistTagForCliVersion as y };
807
1510
 
808
- //# sourceMappingURL=oclif-B7lBzh3R.mjs.map
1511
+ //# sourceMappingURL=oclif-BoUVygsZ.mjs.map