@ait-co/console-cli 0.1.29 → 0.1.31

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.en.md CHANGED
@@ -160,21 +160,34 @@ Every command accepts `--json`. When set:
160
160
 
161
161
  ## Telemetry
162
162
 
163
- `aitcc` can collect optional anonymous usage statistics. **Opt-in only by default** on first run the CLI prompts for consent in a TTY, and silently defaults to deny in CI/pipe environments.
163
+ `aitcc` collects anonymous usage statistics split into two tiers. See the [privacy page](https://docs.aitc.dev/privacy) for details.
164
164
 
165
- Collected: command name, version, platform, random anonymous ID. No personally identifiable information (email, session, user ID, etc.) is ever sent. See the [privacy page](https://docs.aitc.dev/privacy) for details.
165
+ ### Tier 0 anonymous daily ping (on by default, opt-out)
166
+
167
+ Once per day per machine, a minimal anonymous ping is sent on every invocation. Collected: `{source, version, platform}`. No PII, no `anon_id` — the server derives a daily hash from IP + User-Agent using a rotating salt, and stores nothing else. This is the minimum signal needed to know "is anyone actually using this version?"
168
+
169
+ Three ways to opt out:
170
+
171
+ - `AITC_TELEMETRY=off` environment variable — disables all telemetry for this shell session
172
+ - `--no-telemetry` flag — disables for this single invocation only (not permanent)
173
+ - `aitcc telemetry tier0-off` — permanently opts out (persisted to the state file)
174
+
175
+ ### Tier 1 — detailed events (off by default, opt-in)
176
+
177
+ On first run in a TTY, the CLI prompts for consent. In CI or pipe environments it silently defaults to deny. Collected: command name, version, platform, random persistent anonymous ID (`anon_id`). No personally identifiable information (email, session, user ID, etc.) is ever sent.
166
178
 
167
179
  ```sh
168
- aitcc telemetry status # show current consent state + anon ID
169
- aitcc telemetry enable # enable usage statistics
170
- aitcc telemetry disable # disable
171
- aitcc telemetry delete # request deletion of server-side data + rotate local anon ID
180
+ aitcc telemetry status # show both tier status + anon ID
181
+ aitcc telemetry status --json # machine-readable output
182
+ aitcc telemetry enable # enable Tier 1 events
183
+ aitcc telemetry disable # disable Tier 1 events
184
+ aitcc telemetry delete # request deletion of Tier 1 server data + rotate local anon ID
185
+ aitcc telemetry tier0-off # permanently opt out of Tier 0 daily ping
186
+ aitcc telemetry tier0-on # re-enable Tier 0 after a previous tier0-off
172
187
  ```
173
188
 
174
189
  State file: `$XDG_CONFIG_HOME/aitcc/telemetry.json` (fallback `~/.config/aitcc/telemetry.json`, mode `0600`).
175
190
 
176
- > **Note**: events will be rejected server-side until the metrics-ingest `source` allowlist is updated to include `console-cli`. The client-side code is ready.
177
-
178
191
  ## Status
179
192
 
180
193
  `login`, `logout`, `whoami`, and `upgrade` are implemented end-to-end — `login` drives a real browser over CDP and `whoami` reads the live console member API. `deploy`, `logs`, `status` are next. See the [organization landing page](https://aitc.dev/) for the full roadmap.
package/README.md CHANGED
@@ -160,21 +160,34 @@ aitcc app deploy --bundle ./dist/app.zip --json
160
160
 
161
161
  ## 텔레메트리
162
162
 
163
- `aitcc`는 선택적 익명 사용 통계를 수집할 있습니다. **기본값은 비활성(opt-in)** — 처음 실행 시 TTY 환경에서만 동의를 묻고, CI/파이프 환경에선 자동으로 비활성화됩니다.
163
+ `aitcc`는 단계로 분리된 익명 사용 통계를 수집합니다. 자세한 내용은 [privacy 페이지](https://docs.aitc.dev/privacy) 참조.
164
164
 
165
- 수집하는 정보: 실행된 명령 이름, 버전, 플랫폼, 임의 익명 ID. 개인 식별 정보(이메일, 세션, 사용자 ID 등)는 절대 전송하지 않습니다. 자세한 내용은 [privacy 페이지](https://docs.aitc.dev/privacy) 참조.
165
+ ### Tier 0 일별 익명 (기본 ON, opt-out)
166
+
167
+ 매 실행 시 하루 한 번 익명 핑을 보냅니다. 수집 항목: `{source, version, platform}`. 개인 식별 정보 없음. `anon_id`도 없음 — 서버가 일별 salt로 IP+UA 해시를 계산해 저장하며, 그 외 정보는 저장하지 않습니다. "이 버전을 실제로 쓰는 사람이 있는가"를 파악하기 위한 최소 신호입니다.
168
+
169
+ opt-out 방법 (세 가지):
170
+
171
+ - `AITC_TELEMETRY=off` 환경 변수 — 이 쉘 세션 전체 비활성
172
+ - `--no-telemetry` 플래그 — 이 invocation만 비활성 (영구 X)
173
+ - `aitcc telemetry tier0-off` — 영구 opt-out (state file에 저장)
174
+
175
+ ### Tier 1 — 세부 이벤트 (기본 OFF, opt-in)
176
+
177
+ 처음 실행 시 TTY 환경에서만 동의를 묻습니다. CI/파이프 환경에선 자동으로 비활성화됩니다. 수집 항목: 실행된 명령 이름, 버전, 플랫폼, 임의 익명 ID (`anon_id`). 개인 식별 정보(이메일, 세션, 사용자 ID 등)는 절대 전송하지 않습니다.
166
178
 
167
179
  ```sh
168
- aitcc telemetry status # 현재 동의 상태 + 익명 ID 확인
169
- aitcc telemetry enable # 통계 수집 활성화
170
- aitcc telemetry disable # 비활성화
171
- aitcc telemetry delete # 서버에 저장된 데이터 삭제 요청 + 로컬 익명 ID 교체
180
+ aitcc telemetry status # tier 상태 + 익명 ID 확인
181
+ aitcc telemetry status --json # machine-readable 출력
182
+ aitcc telemetry enable # Tier 1 활성화
183
+ aitcc telemetry disable # Tier 1 비활성화
184
+ aitcc telemetry delete # 서버에 저장된 Tier 1 데이터 삭제 요청 + 로컬 익명 ID 교체
185
+ aitcc telemetry tier0-off # Tier 0 익명 핑 영구 비활성화
186
+ aitcc telemetry tier0-on # Tier 0 다시 활성화
172
187
  ```
173
188
 
174
189
  상태 파일: `$XDG_CONFIG_HOME/aitcc/telemetry.json` (fallback `~/.config/aitcc/telemetry.json`, mode `0600`).
175
190
 
176
- > **참고**: metrics-ingest 서버의 `source` allowlist가 `console-cli`를 포함하도록 업데이트되기 전까지는 이벤트가 서버에서 거부될 수 있습니다. 클라이언트 측 코드는 이미 준비돼 있습니다.
177
-
178
191
  ## 진행 상황
179
192
 
180
193
  `login`, `logout`, `whoami`, `upgrade`는 end-to-end 동작 — `login`은 CDP로 실제 브라우저를 띄우고 `whoami`는 live console member API를 호출합니다. `deploy`, `logs`, `status`가 다음 작업입니다. 전체 로드맵은 [organization landing page](https://aitc.dev/) 참조.
package/dist/cli.mjs CHANGED
@@ -7706,6 +7706,29 @@ const FILL_AND_SUBMIT_FN = `
7706
7706
  }
7707
7707
  return null;
7708
7708
  }
7709
+ function pickByAccessibleLabel(textInputOnly, patterns) {
7710
+ const inputs = Array.from(document.querySelectorAll('input'));
7711
+ return inputs.find(i => {
7712
+ const type = (i.type || '').toLowerCase();
7713
+ if (textInputOnly && type !== 'text' && type !== 'email') return false;
7714
+ if (!textInputOnly && type !== 'password') return false;
7715
+ const label = (i.getAttribute('aria-label') || '') + ' ' + (i.placeholder || '');
7716
+ return patterns.some(p => p.test(label));
7717
+ }) || null;
7718
+ }
7719
+ function pickEmailFromPasswordForm(passwordInput) {
7720
+ const form = passwordInput && passwordInput.closest('form');
7721
+ if (!form) return null;
7722
+ const inputs = Array.from(form.querySelectorAll('input'));
7723
+ const passwordIdx = inputs.indexOf(passwordInput);
7724
+ if (passwordIdx < 0) return null;
7725
+ for (let i = 0; i < passwordIdx; i++) {
7726
+ const el = inputs[i];
7727
+ const type = (el.type || '').toLowerCase();
7728
+ if (type === 'text' || type === 'email') return el;
7729
+ }
7730
+ return null;
7731
+ }
7709
7732
  function setNative(input, value) {
7710
7733
  const proto = Object.getPrototypeOf(input);
7711
7734
  const desc = Object.getOwnPropertyDescriptor(proto, 'value');
@@ -7714,12 +7737,15 @@ const FILL_AND_SUBMIT_FN = `
7714
7737
  input.dispatchEvent(new Event('input', { bubbles: true }));
7715
7738
  input.dispatchEvent(new Event('change', { bubbles: true }));
7716
7739
  }
7717
- const emailInput =
7718
- pickByName(['email', 'loginId', 'username']) ||
7719
- pickInputByType(['email']);
7720
7740
  const passwordInput =
7721
7741
  pickByName(['password', 'loginPassword']) ||
7722
- pickInputByType(['password']);
7742
+ pickInputByType(['password']) ||
7743
+ pickByAccessibleLabel(false, [/비밀번호/, /password/i]);
7744
+ const emailInput =
7745
+ pickByName(['email', 'loginId', 'username']) ||
7746
+ pickInputByType(['email']) ||
7747
+ pickByAccessibleLabel(true, [/이메일/, /\\bID\\b/, /email/i, /로그인.{0,5}(아이디|ID)/i]) ||
7748
+ pickEmailFromPasswordForm(passwordInput);
7723
7749
  if (!emailInput) return { ok: false, stage: 'find-email' };
7724
7750
  if (!passwordInput) return { ok: false, stage: 'find-password' };
7725
7751
  setNative(emailInput, email);
@@ -7753,9 +7779,13 @@ const FORM_READY_PROBE_FN = `
7753
7779
  const type = (i.type || '').toLowerCase();
7754
7780
  const placeholder = (i.placeholder || '').toLowerCase();
7755
7781
  const id = (i.id || '').toLowerCase();
7782
+ const aria = (i.getAttribute('aria-label') || '').toLowerCase();
7756
7783
  if (name === 'email' || name === 'loginid' || name === 'username') return true;
7757
7784
  if (type === 'email') return true;
7758
- if (type === 'text' && /id|email|username/.test(name + ' ' + id + ' ' + placeholder)) return true;
7785
+ if (type === 'text') {
7786
+ const blob = name + ' ' + id + ' ' + placeholder + ' ' + aria;
7787
+ if (/id|email|username|이메일|아이디/.test(blob)) return true;
7788
+ }
7759
7789
  return false;
7760
7790
  });
7761
7791
  const hasPassword = inputs.some(i =>
@@ -9199,7 +9229,7 @@ function resolveVersion() {
9199
9229
  if (typeof injected === "string" && injected.length > 0) return injected;
9200
9230
  } catch {}
9201
9231
  try {
9202
- return "0.1.29";
9232
+ return "0.1.31";
9203
9233
  } catch {}
9204
9234
  return "0.0.0-dev";
9205
9235
  }
@@ -9213,7 +9243,7 @@ const VERSION = resolveVersion();
9213
9243
  * Consistent with devtools' localStorage schema names where applicable.
9214
9244
  */
9215
9245
  /** Current policy version. Bump whenever the privacy policy changes. */
9216
- const CURRENT_POLICY_VERSION = "2026-05-12";
9246
+ const CURRENT_POLICY_VERSION = "2026-05-18";
9217
9247
  function telemetryFilePath() {
9218
9248
  return join(configDir(), "telemetry.json");
9219
9249
  }
@@ -9239,7 +9269,9 @@ async function readStateFile() {
9239
9269
  schemaVersion: 1,
9240
9270
  consent: obj.consent,
9241
9271
  policyVersion: obj.policyVersion,
9242
- ...typeof obj.anonId === "string" ? { anonId: obj.anonId } : {}
9272
+ ...typeof obj.anonId === "string" ? { anonId: obj.anonId } : {},
9273
+ ...typeof obj.tier0LastSent === "string" ? { tier0LastSent: obj.tier0LastSent } : {},
9274
+ ...obj.tier0OptOut === true ? { tier0OptOut: true } : {}
9243
9275
  };
9244
9276
  }
9245
9277
  async function writeStateFile(state) {
@@ -9272,7 +9304,7 @@ async function resolveEffectiveConsent() {
9272
9304
  const s = await readStateFile();
9273
9305
  if (!s) return "undecided";
9274
9306
  if (s.consent === "granted") {
9275
- if (s.policyVersion !== "2026-05-12") return "undecided";
9307
+ if (s.policyVersion !== "2026-05-18") return "undecided";
9276
9308
  return "granted";
9277
9309
  }
9278
9310
  return s.consent;
@@ -9289,7 +9321,7 @@ async function getOrCreateAnonId() {
9289
9321
  ...s ?? {
9290
9322
  schemaVersion: 1,
9291
9323
  consent: "undecided",
9292
- policyVersion: "2026-05-12"
9324
+ policyVersion: "2026-05-18"
9293
9325
  },
9294
9326
  anonId: id
9295
9327
  });
@@ -9312,6 +9344,45 @@ async function denyConsent() {
9312
9344
  ...s?.anonId ? { anonId: s.anonId } : {}
9313
9345
  });
9314
9346
  }
9347
+ /** Returns true if Tier 0 pings are permanently opted out. */
9348
+ async function isTier0OptedOut() {
9349
+ return (await readStateFile())?.tier0OptOut === true;
9350
+ }
9351
+ /** Permanently opt out of Tier 0 pings (sets tier0OptOut: true). */
9352
+ async function setTier0OptOut(optOut) {
9353
+ const current = await readStateFile() ?? {
9354
+ schemaVersion: 1,
9355
+ consent: "undecided",
9356
+ policyVersion: "2026-05-18"
9357
+ };
9358
+ if (optOut) await writeStateFile({
9359
+ ...current,
9360
+ tier0OptOut: true
9361
+ });
9362
+ else {
9363
+ const { tier0OptOut: _removed, ...rest } = current;
9364
+ await writeStateFile(rest);
9365
+ }
9366
+ }
9367
+ /**
9368
+ * Returns the ISO date (YYYY-MM-DD) of the last sent Tier 0 ping, or null.
9369
+ */
9370
+ async function getTier0LastSent() {
9371
+ return (await readStateFile())?.tier0LastSent ?? null;
9372
+ }
9373
+ /**
9374
+ * Record that a Tier 0 ping was sent today (ISO date marker).
9375
+ */
9376
+ async function markTier0Sent(date) {
9377
+ await writeStateFile({
9378
+ ...await readStateFile() ?? {
9379
+ schemaVersion: 1,
9380
+ consent: "undecided",
9381
+ policyVersion: "2026-05-18"
9382
+ },
9383
+ tier0LastSent: date
9384
+ });
9385
+ }
9315
9386
  /**
9316
9387
  * Delete data: send DELETE /e?anon_id=... to the server (if we have an id),
9317
9388
  * then rotate local anon_id so subsequent events are unlinkable.
@@ -9335,7 +9406,12 @@ async function deleteMyData(endpoint) {
9335
9406
  /**
9336
9407
  * Telemetry send — fire-and-forget with one retry.
9337
9408
  *
9338
- * Rules:
9409
+ * Tier 0 rules:
9410
+ * 1. No consent check — fires regardless of opt-in state (unless opted out via tier0OptOut).
9411
+ * 2. No anon_id — server generates a daily hash.
9412
+ * 3. 5 s timeout, no retry. Drops silently on any failure.
9413
+ *
9414
+ * Tier 1 rules:
9339
9415
  * 1. If consent ≠ "granted" — drop silently.
9340
9416
  * 2. POST event as JSON with 5 s timeout.
9341
9417
  * 3. On network error or non-2xx: retry ONCE after 2 s. On second failure: drop.
@@ -9372,13 +9448,14 @@ function delay(ms) {
9372
9448
  /** Retry delay in ms — injectable for tests. */
9373
9449
  let RETRY_DELAY_MS = 2e3;
9374
9450
  /**
9375
- * Send a telemetry event. Drops silently if consent is not 'granted'.
9451
+ * Send a Tier 1 telemetry event. Drops silently if consent is not 'granted'.
9376
9452
  * Returns a Promise but callers should NOT await it — fire-and-forget only.
9377
9453
  */
9378
9454
  async function send(endpoint, event, version, meta) {
9379
9455
  if (await readConsentState() !== "granted") return;
9380
9456
  const sanitized = sanitizeMeta(meta);
9381
9457
  const payload = {
9458
+ tier: 1,
9382
9459
  source: "console-cli",
9383
9460
  event,
9384
9461
  anon_id: await getOrCreateAnonId(),
@@ -9390,15 +9467,45 @@ async function send(endpoint, event, version, meta) {
9390
9467
  await delay(RETRY_DELAY_MS);
9391
9468
  await doFetch(endpoint, payload);
9392
9469
  }
9470
+ /**
9471
+ * Send a Tier 0 anonymous daily ping.
9472
+ * - No anon_id (server generates a daily hash from IP+UA).
9473
+ * - No retry. 5 s timeout. Fire-and-forget.
9474
+ * - Callers should NOT await this.
9475
+ */
9476
+ async function sendTier0Ping(endpoint, version) {
9477
+ const payload = {
9478
+ tier: 0,
9479
+ source: "console-cli",
9480
+ version,
9481
+ platform: process.platform
9482
+ };
9483
+ const controller = new AbortController();
9484
+ const timeoutId = setTimeout(() => controller.abort(), 5e3);
9485
+ try {
9486
+ await fetch(`${endpoint}/e`, {
9487
+ method: "POST",
9488
+ headers: { "Content-Type": "application/json" },
9489
+ body: JSON.stringify(payload),
9490
+ signal: controller.signal
9491
+ });
9492
+ } catch {} finally {
9493
+ clearTimeout(timeoutId);
9494
+ }
9495
+ }
9393
9496
  //#endregion
9394
9497
  //#region src/telemetry/index.ts
9395
9498
  /**
9396
9499
  * Telemetry client — internal to @ait-co/console-cli.
9397
9500
  *
9398
- * Usage: import { trackInvocation, trackInstall } from './telemetry/index.js'
9501
+ * Usage: import { trackInvocation, trackTier0Ping } from './telemetry/index.js'
9502
+ *
9503
+ * Tier 0 (opt-out): anonymous daily ping. Fires on every invocation; client-side
9504
+ * daily dedupe via tier0LastSent. Respects AITC_TELEMETRY=off, --no-telemetry,
9505
+ * and permanent tier0OptOut flag.
9399
9506
  *
9400
- * Events are fire-and-forget (non-blocking). Consent is opt-in only.
9401
- * First invocation on a TTY prompts the user; non-TTY (CI) defaults to deny.
9507
+ * Tier 1 (opt-in): detailed events. First invocation on a TTY prompts the user;
9508
+ * non-TTY (CI) defaults to deny.
9402
9509
  *
9403
9510
  * Endpoint override for staging: AITCC_TELEMETRY_ENV=staging
9404
9511
  * (or automatically when VERSION contains '-dev').
@@ -9442,6 +9549,37 @@ async function promptConsent() {
9442
9549
  rl.close();
9443
9550
  }
9444
9551
  }
9552
+ /**
9553
+ * Check whether telemetry is globally disabled via environment or CLI flag.
9554
+ * Accepts the parsed --no-telemetry flag value from argv.
9555
+ */
9556
+ function isTelemetryGloballyDisabled(noTelemetryFlag) {
9557
+ if (noTelemetryFlag) return true;
9558
+ const env = process.env.AITC_TELEMETRY;
9559
+ if (env !== void 0 && env.toLowerCase() === "off") return true;
9560
+ return false;
9561
+ }
9562
+ /**
9563
+ * Send a Tier 0 anonymous daily ping (fire-and-forget).
9564
+ *
9565
+ * Skips if:
9566
+ * - AITC_TELEMETRY=off or --no-telemetry flag
9567
+ * - tier0OptOut === true in the state file
9568
+ * - already sent today (tier0LastSent === today's ISO date)
9569
+ *
9570
+ * On success, records today's date in tier0LastSent for client-side daily dedupe.
9571
+ * The server also deduplicates server-side via KV, so this is an extra client guard.
9572
+ */
9573
+ async function trackTier0Ping(noTelemetryFlag = false) {
9574
+ try {
9575
+ if (isTelemetryGloballyDisabled(noTelemetryFlag)) return;
9576
+ if (await isTier0OptedOut()) return;
9577
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
9578
+ if (await getTier0LastSent() === today) return;
9579
+ sendTier0Ping(TELEMETRY_ENDPOINT, VERSION);
9580
+ await markTier0Sent(today);
9581
+ } catch {}
9582
+ }
9445
9583
  /** True only on the first invocation after a fresh install. */
9446
9584
  async function isNewInstall() {
9447
9585
  const markerPath = `${telemetryFilePath()}.install`;
@@ -9455,11 +9593,14 @@ async function isNewInstall() {
9455
9593
  }
9456
9594
  /**
9457
9595
  * Called at CLI entry point with the resolved top-level command name.
9458
- * Handles first-run consent prompt, install detection, and event send.
9596
+ * Handles first-run Tier 1 consent prompt, install detection, and Tier 1 event send.
9459
9597
  * Fire-and-forget: do NOT await this.
9598
+ *
9599
+ * Note: Tier 0 ping is sent separately via trackTier0Ping() before this call.
9460
9600
  */
9461
- async function trackInvocation(command) {
9601
+ async function trackInvocation(command, noTelemetryFlag = false) {
9462
9602
  try {
9603
+ if (isTelemetryGloballyDisabled(noTelemetryFlag)) return;
9463
9604
  if (isFirstRun()) await promptConsent();
9464
9605
  if (await resolveEffectiveConsent() !== "granted") return;
9465
9606
  if (await isNewInstall()) send(TELEMETRY_ENDPOINT, "cli_install", VERSION, {
@@ -9472,13 +9613,13 @@ async function trackInvocation(command) {
9472
9613
  const telemetryCommand = defineCommand({
9473
9614
  meta: {
9474
9615
  name: "telemetry",
9475
- description: "Manage anonymous usage telemetry (opt-in)."
9616
+ description: "Manage anonymous usage telemetry."
9476
9617
  },
9477
9618
  subCommands: {
9478
9619
  status: defineCommand({
9479
9620
  meta: {
9480
9621
  name: "status",
9481
- description: "Show current telemetry consent state and anon_id."
9622
+ description: "Show current telemetry status for both Tier 0 and Tier 1."
9482
9623
  },
9483
9624
  args: { json: {
9484
9625
  type: "boolean",
@@ -9486,24 +9627,37 @@ const telemetryCommand = defineCommand({
9486
9627
  default: false
9487
9628
  } },
9488
9629
  async run({ args }) {
9489
- const consent = await resolveEffectiveConsent();
9490
- const anonId = consent === "granted" ? await getOrCreateAnonId() : null;
9630
+ const [tier1Consent, tier0OptOut, tier0LastSent, anonId] = await Promise.all([
9631
+ resolveEffectiveConsent(),
9632
+ isTier0OptedOut(),
9633
+ getTier0LastSent(),
9634
+ resolveEffectiveConsent().then((c) => c === "granted" ? getOrCreateAnonId() : null)
9635
+ ]);
9491
9636
  const filePath = telemetryFilePath();
9637
+ const tier0Status = tier0OptOut ? "off (opted out)" : "on";
9638
+ const tier0Display = tier0LastSent ? `${tier0Status} (last sent: ${tier0LastSent})` : tier0Status;
9492
9639
  if (args.json) {
9493
9640
  process.stdout.write(`${JSON.stringify({
9494
9641
  ok: true,
9495
- consent,
9496
- policyVersion: CURRENT_POLICY_VERSION,
9642
+ tier0: {
9643
+ status: tier0OptOut ? "opted-out" : "on",
9644
+ lastSent: tier0LastSent ?? null
9645
+ },
9646
+ tier1: {
9647
+ consent: tier1Consent,
9648
+ policyVersion: CURRENT_POLICY_VERSION,
9649
+ ...anonId ? { anonId } : {}
9650
+ },
9497
9651
  endpoint: TELEMETRY_ENDPOINT,
9498
- ...anonId ? { anonId } : {},
9499
9652
  filePath
9500
9653
  })}\n`);
9501
9654
  return exitAfterFlush(ExitCode.Ok);
9502
9655
  }
9503
- process.stdout.write(`Telemetry: ${consent}\n`);
9504
- process.stdout.write(`Policy version: ${CURRENT_POLICY_VERSION}\n`);
9505
- process.stdout.write(`Endpoint: ${TELEMETRY_ENDPOINT}\n`);
9506
- if (anonId) process.stdout.write(`Anon ID: ${anonId}\n`);
9656
+ process.stdout.write("Telemetry status\n");
9657
+ process.stdout.write(` Tier 0 (anonymous daily ping): ${tier0Display}\n`);
9658
+ process.stdout.write(` Tier 1 (opt-in events): ${tier1Consent} (policyVersion: ${CURRENT_POLICY_VERSION})\n`);
9659
+ process.stdout.write(`\nEndpoint: ${TELEMETRY_ENDPOINT}\n`);
9660
+ if (anonId) process.stdout.write(`Anon ID: ${anonId}\n`);
9507
9661
  process.stdout.write(`State file: ${filePath}\n`);
9508
9662
  return exitAfterFlush(ExitCode.Ok);
9509
9663
  }
@@ -9575,6 +9729,46 @@ const telemetryCommand = defineCommand({
9575
9729
  else process.stderr.write("Deletion request failed. Please try again or contact the maintainers.\n");
9576
9730
  return exitAfterFlush(ok || beforeConsent === "undecided" ? ExitCode.Ok : ExitCode.NetworkError);
9577
9731
  }
9732
+ }),
9733
+ "tier0-off": defineCommand({
9734
+ meta: {
9735
+ name: "tier0-off",
9736
+ description: "Permanently opt out of the Tier 0 anonymous daily ping."
9737
+ },
9738
+ args: { json: {
9739
+ type: "boolean",
9740
+ description: "Emit machine-readable JSON.",
9741
+ default: false
9742
+ } },
9743
+ async run({ args }) {
9744
+ await setTier0OptOut(true);
9745
+ if (args.json) process.stdout.write(`${JSON.stringify({
9746
+ ok: true,
9747
+ tier0: { status: "opted-out" }
9748
+ })}\n`);
9749
+ else process.stdout.write("Tier 0 anonymous ping disabled. No daily pings will be sent.\n");
9750
+ return exitAfterFlush(ExitCode.Ok);
9751
+ }
9752
+ }),
9753
+ "tier0-on": defineCommand({
9754
+ meta: {
9755
+ name: "tier0-on",
9756
+ description: "Re-enable the Tier 0 anonymous daily ping after a previous tier0-off."
9757
+ },
9758
+ args: { json: {
9759
+ type: "boolean",
9760
+ description: "Emit machine-readable JSON.",
9761
+ default: false
9762
+ } },
9763
+ async run({ args }) {
9764
+ await setTier0OptOut(false);
9765
+ if (args.json) process.stdout.write(`${JSON.stringify({
9766
+ ok: true,
9767
+ tier0: { status: "on" }
9768
+ })}\n`);
9769
+ else process.stdout.write("Tier 0 anonymous ping re-enabled.\n");
9770
+ return exitAfterFlush(ExitCode.Ok);
9771
+ }
9578
9772
  })
9579
9773
  }
9580
9774
  });
@@ -10864,7 +11058,10 @@ const main = defineCommand({
10864
11058
  }
10865
11059
  });
10866
11060
  cleanupStaleUpgradeArtifacts().catch(() => {});
10867
- trackInvocation(process.argv.slice(2).find((a) => !a.startsWith("-")) ?? "(none)");
11061
+ const _telemetryCmd = process.argv.slice(2).find((a) => !a.startsWith("-")) ?? "(none)";
11062
+ const _noTelemetry = process.argv.includes("--no-telemetry");
11063
+ trackTier0Ping(_noTelemetry);
11064
+ trackInvocation(_telemetryCmd, _noTelemetry);
10868
11065
  runMain(main);
10869
11066
  //#endregion
10870
11067
  export {};