@ait-co/console-cli 0.1.28 → 0.1.29

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/dist/cli.mjs CHANGED
@@ -8,7 +8,8 @@ import { unzipSync } from "fflate";
8
8
  import { checkbox, confirm, editor, input, password, select } from "@inquirer/prompts";
9
9
  import { imageSize } from "image-size";
10
10
  import { execFile, spawn } from "node:child_process";
11
- import { constants, createReadStream } from "node:fs";
11
+ import { constants, createReadStream, existsSync } from "node:fs";
12
+ import { createInterface } from "node:readline/promises";
12
13
  import { promisify } from "node:util";
13
14
  import { createHash } from "node:crypto";
14
15
  //#region src/api/http.ts
@@ -3601,7 +3602,7 @@ function serviceStatusFor(entry) {
3601
3602
  }
3602
3603
  const POLL_MIN_INTERVAL_SEC = 30;
3603
3604
  const POLL_MAX_INTERVAL_SEC = 3600;
3604
- const statusCommand$1 = defineCommand({
3605
+ const statusCommand$2 = defineCommand({
3605
3606
  meta: {
3606
3607
  name: "status",
3607
3608
  description: "Show the derived review state of a mini-app (under-review / rejected / approved)."
@@ -5702,7 +5703,7 @@ const appCommand = defineCommand({
5702
5703
  }),
5703
5704
  ls: lsCommand$4,
5704
5705
  show: showCommand$2,
5705
- status: statusCommand$1,
5706
+ status: statusCommand$2,
5706
5707
  ratings: ratingsCommand,
5707
5708
  reports: reportsCommand,
5708
5709
  bundles: bundlesCommand,
@@ -9191,6 +9192,393 @@ const noticesCommand = defineCommand({
9191
9192
  }
9192
9193
  });
9193
9194
  //#endregion
9195
+ //#region src/version.ts
9196
+ function resolveVersion() {
9197
+ try {
9198
+ const injected = globalThis.AITCC_VERSION;
9199
+ if (typeof injected === "string" && injected.length > 0) return injected;
9200
+ } catch {}
9201
+ try {
9202
+ return "0.1.29";
9203
+ } catch {}
9204
+ return "0.0.0-dev";
9205
+ }
9206
+ const VERSION = resolveVersion();
9207
+ //#endregion
9208
+ //#region src/telemetry/state.ts
9209
+ /**
9210
+ * Telemetry consent state + anon_id I/O for console-cli.
9211
+ *
9212
+ * Storage: ~/.config/aitcc/telemetry.json (0600, XDG-aware via configDir())
9213
+ * Consistent with devtools' localStorage schema names where applicable.
9214
+ */
9215
+ /** Current policy version. Bump whenever the privacy policy changes. */
9216
+ const CURRENT_POLICY_VERSION = "2026-05-12";
9217
+ function telemetryFilePath() {
9218
+ return join(configDir(), "telemetry.json");
9219
+ }
9220
+ async function readStateFile() {
9221
+ let raw;
9222
+ try {
9223
+ raw = await readFile(telemetryFilePath(), "utf8");
9224
+ } catch {
9225
+ return null;
9226
+ }
9227
+ let parsed;
9228
+ try {
9229
+ parsed = JSON.parse(raw);
9230
+ } catch {
9231
+ return null;
9232
+ }
9233
+ if (!parsed || typeof parsed !== "object") return null;
9234
+ const obj = parsed;
9235
+ if (obj.schemaVersion !== 1) return null;
9236
+ if (obj.consent !== "granted" && obj.consent !== "denied" && obj.consent !== "undecided") return null;
9237
+ if (typeof obj.policyVersion !== "string") return null;
9238
+ return {
9239
+ schemaVersion: 1,
9240
+ consent: obj.consent,
9241
+ policyVersion: obj.policyVersion,
9242
+ ...typeof obj.anonId === "string" ? { anonId: obj.anonId } : {}
9243
+ };
9244
+ }
9245
+ async function writeStateFile(state) {
9246
+ const path = telemetryFilePath();
9247
+ await mkdir(dirname(path), {
9248
+ recursive: true,
9249
+ mode: 448
9250
+ });
9251
+ const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
9252
+ try {
9253
+ await writeFile(tmp, JSON.stringify(state, null, 2), { mode: 384 });
9254
+ await rename(tmp, path);
9255
+ } catch (err) {
9256
+ await unlink(tmp).catch(() => {});
9257
+ throw err;
9258
+ }
9259
+ }
9260
+ /** Read the raw consent from disk. Returns 'undecided' if no file. */
9261
+ async function readConsentState() {
9262
+ const s = await readStateFile();
9263
+ if (!s) return "undecided";
9264
+ return s.consent;
9265
+ }
9266
+ /**
9267
+ * Resolve effective consent with policy-version bump rule:
9268
+ * - Previously 'granted' but on an old policy version → revert to 'undecided'
9269
+ * - Previously 'denied' on any version → stay 'denied'
9270
+ */
9271
+ async function resolveEffectiveConsent() {
9272
+ const s = await readStateFile();
9273
+ if (!s) return "undecided";
9274
+ if (s.consent === "granted") {
9275
+ if (s.policyVersion !== "2026-05-12") return "undecided";
9276
+ return "granted";
9277
+ }
9278
+ return s.consent;
9279
+ }
9280
+ /**
9281
+ * Returns the stored anon_id, or generates + persists a new UUID v4.
9282
+ * Once generated it is never overwritten except after a successful deleteMyData call.
9283
+ */
9284
+ async function getOrCreateAnonId() {
9285
+ const s = await readStateFile();
9286
+ if (s?.anonId) return s.anonId;
9287
+ const id = crypto.randomUUID();
9288
+ await writeStateFile({
9289
+ ...s ?? {
9290
+ schemaVersion: 1,
9291
+ consent: "undecided",
9292
+ policyVersion: "2026-05-12"
9293
+ },
9294
+ anonId: id
9295
+ });
9296
+ return id;
9297
+ }
9298
+ async function acceptConsent() {
9299
+ await writeStateFile({
9300
+ schemaVersion: 1,
9301
+ consent: "granted",
9302
+ policyVersion: CURRENT_POLICY_VERSION,
9303
+ anonId: (await readStateFile())?.anonId ?? crypto.randomUUID()
9304
+ });
9305
+ }
9306
+ async function denyConsent() {
9307
+ const s = await readStateFile();
9308
+ await writeStateFile({
9309
+ schemaVersion: 1,
9310
+ consent: "denied",
9311
+ policyVersion: CURRENT_POLICY_VERSION,
9312
+ ...s?.anonId ? { anonId: s.anonId } : {}
9313
+ });
9314
+ }
9315
+ /**
9316
+ * Delete data: send DELETE /e?anon_id=... to the server (if we have an id),
9317
+ * then rotate local anon_id so subsequent events are unlinkable.
9318
+ */
9319
+ async function deleteMyData(endpoint) {
9320
+ const s = await readStateFile();
9321
+ if (!s?.anonId) return false;
9322
+ try {
9323
+ if (!(await fetch(`${endpoint}/e?anon_id=${encodeURIComponent(s.anonId)}`, { method: "DELETE" })).ok) return false;
9324
+ await writeStateFile({
9325
+ ...s,
9326
+ anonId: crypto.randomUUID()
9327
+ });
9328
+ return true;
9329
+ } catch {
9330
+ return false;
9331
+ }
9332
+ }
9333
+ //#endregion
9334
+ //#region src/telemetry/send.ts
9335
+ /**
9336
+ * Telemetry send — fire-and-forget with one retry.
9337
+ *
9338
+ * Rules:
9339
+ * 1. If consent ≠ "granted" — drop silently.
9340
+ * 2. POST event as JSON with 5 s timeout.
9341
+ * 3. On network error or non-2xx: retry ONCE after 2 s. On second failure: drop.
9342
+ * 4. Meta is capped at 256 bytes (JSON-serialized); oversized meta is dropped.
9343
+ * 5. All calls are non-blocking — caller never awaits send().
9344
+ */
9345
+ /** Meta size cap per server contract (JSON bytes). */
9346
+ const META_BYTE_CAP = 256;
9347
+ function sanitizeMeta(meta) {
9348
+ if (meta === void 0) return void 0;
9349
+ const serialized = JSON.stringify(meta);
9350
+ if (new TextEncoder().encode(serialized).byteLength > META_BYTE_CAP) return void 0;
9351
+ return meta;
9352
+ }
9353
+ async function doFetch(endpoint, payload) {
9354
+ const controller = new AbortController();
9355
+ const timeoutId = setTimeout(() => controller.abort(), 5e3);
9356
+ try {
9357
+ return (await fetch(`${endpoint}/e`, {
9358
+ method: "POST",
9359
+ headers: { "Content-Type": "application/json" },
9360
+ body: JSON.stringify(payload),
9361
+ signal: controller.signal
9362
+ })).ok;
9363
+ } catch {
9364
+ return false;
9365
+ } finally {
9366
+ clearTimeout(timeoutId);
9367
+ }
9368
+ }
9369
+ function delay(ms) {
9370
+ return new Promise((resolve) => setTimeout(resolve, ms));
9371
+ }
9372
+ /** Retry delay in ms — injectable for tests. */
9373
+ let RETRY_DELAY_MS = 2e3;
9374
+ /**
9375
+ * Send a telemetry event. Drops silently if consent is not 'granted'.
9376
+ * Returns a Promise but callers should NOT await it — fire-and-forget only.
9377
+ */
9378
+ async function send(endpoint, event, version, meta) {
9379
+ if (await readConsentState() !== "granted") return;
9380
+ const sanitized = sanitizeMeta(meta);
9381
+ const payload = {
9382
+ source: "console-cli",
9383
+ event,
9384
+ anon_id: await getOrCreateAnonId(),
9385
+ version,
9386
+ ts: Date.now(),
9387
+ ...sanitized !== void 0 ? { meta: sanitized } : {}
9388
+ };
9389
+ if (await doFetch(endpoint, payload)) return;
9390
+ await delay(RETRY_DELAY_MS);
9391
+ await doFetch(endpoint, payload);
9392
+ }
9393
+ //#endregion
9394
+ //#region src/telemetry/index.ts
9395
+ /**
9396
+ * Telemetry client — internal to @ait-co/console-cli.
9397
+ *
9398
+ * Usage: import { trackInvocation, trackInstall } from './telemetry/index.js'
9399
+ *
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.
9402
+ *
9403
+ * Endpoint override for staging: AITCC_TELEMETRY_ENV=staging
9404
+ * (or automatically when VERSION contains '-dev').
9405
+ */
9406
+ function resolveEndpoint() {
9407
+ if (process.env.AITCC_TELEMETRY_ENV === "staging") return "https://t-staging.aitc.dev";
9408
+ if (VERSION.includes("-dev")) return "https://t-staging.aitc.dev";
9409
+ return "https://t.aitc.dev";
9410
+ }
9411
+ const TELEMETRY_ENDPOINT = resolveEndpoint();
9412
+ /** Returns true if this is the very first run (telemetry.json does not exist). */
9413
+ function isFirstRun() {
9414
+ return !existsSync(telemetryFilePath());
9415
+ }
9416
+ /**
9417
+ * Prompt for consent on TTY. Defaults to deny on any non-TTY or error.
9418
+ * Called once per install (no file yet) when stdin/stdout are both TTYs.
9419
+ */
9420
+ async function promptConsent() {
9421
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
9422
+ await denyConsent();
9423
+ return;
9424
+ }
9425
+ const rl = createInterface({
9426
+ input: process.stdin,
9427
+ output: process.stdout
9428
+ });
9429
+ try {
9430
+ process.stderr.write([
9431
+ "",
9432
+ "aitcc 사용 개선을 위해 익명 사용 통계를 수집해도 될까요?",
9433
+ " · 개인 식별 정보 없음 · 랜덤 익명 ID만 사용 · 언제든 off 가능",
9434
+ " 자세한 내용: https://docs.aitc.dev/privacy",
9435
+ ""
9436
+ ].join("\n"));
9437
+ if ((await rl.question("보내도 될까요? [y/N] ")).trim().toLowerCase() === "y") await acceptConsent();
9438
+ else await denyConsent();
9439
+ } catch {
9440
+ await denyConsent();
9441
+ } finally {
9442
+ rl.close();
9443
+ }
9444
+ }
9445
+ /** True only on the first invocation after a fresh install. */
9446
+ async function isNewInstall() {
9447
+ const markerPath = `${telemetryFilePath()}.install`;
9448
+ if (existsSync(markerPath)) return false;
9449
+ try {
9450
+ await writeFile(markerPath, "1", { mode: 384 });
9451
+ return true;
9452
+ } catch {
9453
+ return false;
9454
+ }
9455
+ }
9456
+ /**
9457
+ * Called at CLI entry point with the resolved top-level command name.
9458
+ * Handles first-run consent prompt, install detection, and event send.
9459
+ * Fire-and-forget: do NOT await this.
9460
+ */
9461
+ async function trackInvocation(command) {
9462
+ try {
9463
+ if (isFirstRun()) await promptConsent();
9464
+ if (await resolveEffectiveConsent() !== "granted") return;
9465
+ if (await isNewInstall()) send(TELEMETRY_ENDPOINT, "cli_install", VERSION, {
9466
+ platform: process.platform,
9467
+ arch: process.arch
9468
+ });
9469
+ send(TELEMETRY_ENDPOINT, "cli_invoked", VERSION, { command });
9470
+ } catch {}
9471
+ }
9472
+ const telemetryCommand = defineCommand({
9473
+ meta: {
9474
+ name: "telemetry",
9475
+ description: "Manage anonymous usage telemetry (opt-in)."
9476
+ },
9477
+ subCommands: {
9478
+ status: defineCommand({
9479
+ meta: {
9480
+ name: "status",
9481
+ description: "Show current telemetry consent state and anon_id."
9482
+ },
9483
+ args: { json: {
9484
+ type: "boolean",
9485
+ description: "Emit machine-readable JSON.",
9486
+ default: false
9487
+ } },
9488
+ async run({ args }) {
9489
+ const consent = await resolveEffectiveConsent();
9490
+ const anonId = consent === "granted" ? await getOrCreateAnonId() : null;
9491
+ const filePath = telemetryFilePath();
9492
+ if (args.json) {
9493
+ process.stdout.write(`${JSON.stringify({
9494
+ ok: true,
9495
+ consent,
9496
+ policyVersion: CURRENT_POLICY_VERSION,
9497
+ endpoint: TELEMETRY_ENDPOINT,
9498
+ ...anonId ? { anonId } : {},
9499
+ filePath
9500
+ })}\n`);
9501
+ return exitAfterFlush(ExitCode.Ok);
9502
+ }
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`);
9507
+ process.stdout.write(`State file: ${filePath}\n`);
9508
+ return exitAfterFlush(ExitCode.Ok);
9509
+ }
9510
+ }),
9511
+ enable: defineCommand({
9512
+ meta: {
9513
+ name: "enable",
9514
+ description: "Enable anonymous usage telemetry (opt-in)."
9515
+ },
9516
+ args: { json: {
9517
+ type: "boolean",
9518
+ description: "Emit machine-readable JSON.",
9519
+ default: false
9520
+ } },
9521
+ async run({ args }) {
9522
+ await acceptConsent();
9523
+ const anonId = await getOrCreateAnonId();
9524
+ if (args.json) process.stdout.write(`${JSON.stringify({
9525
+ ok: true,
9526
+ consent: "granted",
9527
+ anonId
9528
+ })}\n`);
9529
+ else {
9530
+ process.stdout.write("Telemetry enabled. Thank you!\n");
9531
+ process.stdout.write(`Anon ID: ${anonId}\n`);
9532
+ }
9533
+ return exitAfterFlush(ExitCode.Ok);
9534
+ }
9535
+ }),
9536
+ disable: defineCommand({
9537
+ meta: {
9538
+ name: "disable",
9539
+ description: "Disable anonymous usage telemetry."
9540
+ },
9541
+ args: { json: {
9542
+ type: "boolean",
9543
+ description: "Emit machine-readable JSON.",
9544
+ default: false
9545
+ } },
9546
+ async run({ args }) {
9547
+ await denyConsent();
9548
+ if (args.json) process.stdout.write(`${JSON.stringify({
9549
+ ok: true,
9550
+ consent: "denied"
9551
+ })}\n`);
9552
+ else process.stdout.write("Telemetry disabled.\n");
9553
+ return exitAfterFlush(ExitCode.Ok);
9554
+ }
9555
+ }),
9556
+ delete: defineCommand({
9557
+ meta: {
9558
+ name: "delete",
9559
+ description: "Delete your telemetry data from the server and rotate the local anon_id."
9560
+ },
9561
+ args: { json: {
9562
+ type: "boolean",
9563
+ description: "Emit machine-readable JSON.",
9564
+ default: false
9565
+ } },
9566
+ async run({ args }) {
9567
+ const beforeConsent = await readConsentState();
9568
+ const ok = await deleteMyData(TELEMETRY_ENDPOINT);
9569
+ if (args.json) process.stdout.write(`${JSON.stringify({
9570
+ ok,
9571
+ ...ok ? {} : { reason: beforeConsent === "undecided" ? "no-data" : "server-error" }
9572
+ })}\n`);
9573
+ else if (ok) process.stdout.write("Deletion request sent. Your data has been removed and a new anon ID assigned.\n");
9574
+ else if (beforeConsent === "undecided") process.stdout.write("No telemetry data to delete (telemetry was never enabled).\n");
9575
+ else process.stderr.write("Deletion request failed. Please try again or contact the maintainers.\n");
9576
+ return exitAfterFlush(ok || beforeConsent === "undecided" ? ExitCode.Ok : ExitCode.NetworkError);
9577
+ }
9578
+ })
9579
+ }
9580
+ });
9581
+ //#endregion
9194
9582
  //#region src/github.ts
9195
9583
  const REPO_OWNER = "apps-in-toss-community";
9196
9584
  const REPO_NAME = "console-cli";
@@ -9324,19 +9712,6 @@ function sha256OfFile(path) {
9324
9712
  });
9325
9713
  }
9326
9714
  //#endregion
9327
- //#region src/version.ts
9328
- function resolveVersion() {
9329
- try {
9330
- const injected = globalThis.AITCC_VERSION;
9331
- if (typeof injected === "string" && injected.length > 0) return injected;
9332
- } catch {}
9333
- try {
9334
- return "0.1.28";
9335
- } catch {}
9336
- return "0.0.0-dev";
9337
- }
9338
- const VERSION = resolveVersion();
9339
- //#endregion
9340
9715
  //#region src/commands/upgrade.ts
9341
9716
  const execFileP = promisify(execFile);
9342
9717
  function isStandaloneBinary() {
@@ -10484,10 +10859,12 @@ const main = defineCommand({
10484
10859
  keys: keysCommand,
10485
10860
  notices: noticesCommand,
10486
10861
  me: meCommand,
10862
+ telemetry: telemetryCommand,
10487
10863
  completion: completionCommand
10488
10864
  }
10489
10865
  });
10490
10866
  cleanupStaleUpgradeArtifacts().catch(() => {});
10867
+ trackInvocation(process.argv.slice(2).find((a) => !a.startsWith("-")) ?? "(none)");
10491
10868
  runMain(main);
10492
10869
  //#endregion
10493
10870
  export {};