@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.
- package/README.md +53 -35
- package/SKILLS.md +4 -0
- package/dist/commands/apply.mjs +9 -3
- package/dist/commands/apply.mjs.map +1 -1
- package/dist/commands/doctor.mjs +12 -5
- package/dist/commands/doctor.mjs.map +1 -1
- package/dist/commands/eject.mjs +3 -3
- package/dist/commands/logs.mjs +2 -2
- package/dist/commands/plan.mjs +9 -3
- package/dist/commands/plan.mjs.map +1 -1
- package/dist/commands/reset.mjs +2 -2
- package/dist/commands/setup.mjs +20 -3
- package/dist/commands/setup.mjs.map +1 -1
- package/dist/commands/start.mjs +4 -3
- package/dist/commands/start.mjs.map +1 -1
- package/dist/commands/status.mjs +3 -3
- package/dist/commands/stop.mjs +2 -2
- package/dist/{docker-CnGQK3ZK.mjs → docker-Djz6BLp9.mjs} +2 -2
- package/dist/{docker-CnGQK3ZK.mjs.map → docker-Djz6BLp9.mjs.map} +1 -1
- package/dist/{docker-guidance-ypN3IM3o.mjs → docker-guidance-D-33g1uV.mjs} +2 -2
- package/dist/{docker-guidance-ypN3IM3o.mjs.map → docker-guidance-D-33g1uV.mjs.map} +1 -1
- package/dist/{oclif-B7lBzh3R.mjs → oclif-BoUVygsZ.mjs} +708 -5
- package/dist/oclif-BoUVygsZ.mjs.map +1 -0
- package/dist/{orca-mzcHxDvu.mjs → orca-DU8myWGm.mjs} +59 -52
- package/dist/orca-DU8myWGm.mjs.map +1 -0
- package/dist/{project-Cd0L3PtM.mjs → project-B1qVQMKS.mjs} +2 -2
- package/dist/{project-Cd0L3PtM.mjs.map → project-B1qVQMKS.mjs.map} +1 -1
- package/dist/{sync-BojoQm2P.mjs → sync-CzruNz8K.mjs} +11 -5
- package/dist/sync-CzruNz8K.mjs.map +1 -0
- package/oclif.manifest.json +61 -1
- package/package.json +5 -4
- package/dist/oclif-B7lBzh3R.mjs.map +0 -1
- package/dist/orca-mzcHxDvu.mjs.map +0 -1
- 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 {
|
|
8
|
-
import {
|
|
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-
|
|
1511
|
+
//# sourceMappingURL=oclif-BoUVygsZ.mjs.map
|