@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 +21 -8
- package/README.md +21 -8
- package/dist/cli.mjs +226 -29
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -160,21 +160,34 @@ Every command accepts `--json`. When set:
|
|
|
160
160
|
|
|
161
161
|
## Telemetry
|
|
162
162
|
|
|
163
|
-
`aitcc`
|
|
163
|
+
`aitcc` collects anonymous usage statistics split into two tiers. See the [privacy page](https://docs.aitc.dev/privacy) for details.
|
|
164
164
|
|
|
165
|
-
|
|
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
|
|
169
|
-
aitcc telemetry
|
|
170
|
-
aitcc telemetry
|
|
171
|
-
aitcc telemetry
|
|
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`는
|
|
163
|
+
`aitcc`는 두 단계로 분리된 익명 사용 통계를 수집합니다. 자세한 내용은 [privacy 페이지](https://docs.aitc.dev/privacy) 참조.
|
|
164
164
|
|
|
165
|
-
|
|
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
|
|
169
|
-
aitcc telemetry
|
|
170
|
-
aitcc telemetry
|
|
171
|
-
aitcc telemetry
|
|
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'
|
|
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.
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
*
|
|
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,
|
|
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
|
-
*
|
|
9401
|
-
*
|
|
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
|
|
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
|
|
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
|
|
9490
|
-
|
|
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
|
-
|
|
9496
|
-
|
|
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(
|
|
9504
|
-
process.stdout.write(`
|
|
9505
|
-
process.stdout.write(`
|
|
9506
|
-
|
|
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
|
-
|
|
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 {};
|