@gpc-cli/core 0.9.28 → 0.9.30

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/index.js CHANGED
@@ -40,6 +40,21 @@ var NetworkError = class extends GpcError {
40
40
 
41
41
  // src/output.ts
42
42
  import process2 from "process";
43
+ function isColorEnabled() {
44
+ if (process2.env["NO_COLOR"] !== void 0 && process2.env["NO_COLOR"] !== "") return false;
45
+ const fc = process2.env["FORCE_COLOR"];
46
+ if (fc === "1" || fc === "2" || fc === "3") return true;
47
+ return process2.stdout.isTTY ?? false;
48
+ }
49
+ function ansi(code, s) {
50
+ return isColorEnabled() ? `\x1B[${code}m${s}\x1B[0m` : s;
51
+ }
52
+ function bold(s) {
53
+ return ansi(1, s);
54
+ }
55
+ function dim(s) {
56
+ return ansi(2, s);
57
+ }
43
58
  function detectOutputFormat() {
44
59
  return process2.stdout.isTTY ? "table" : "json";
45
60
  }
@@ -150,10 +165,20 @@ ${formatYaml(value, indent + 1)}`;
150
165
  }
151
166
  return String(data);
152
167
  }
153
- var MAX_CELL_WIDTH = 60;
154
- function truncateCell(value) {
155
- if (value.length <= MAX_CELL_WIDTH) return value;
156
- return value.slice(0, MAX_CELL_WIDTH - 3) + "...";
168
+ var DEFAULT_CELL_WIDTH = 60;
169
+ function computeMaxCellWidth(colCount) {
170
+ const cols = process2.stdout.columns;
171
+ if (cols && colCount > 0) {
172
+ return Math.max(20, Math.floor(cols / colCount) - 2);
173
+ }
174
+ return DEFAULT_CELL_WIDTH;
175
+ }
176
+ function truncateCell(value, maxWidth = DEFAULT_CELL_WIDTH) {
177
+ if (value.length <= maxWidth) return value;
178
+ return value.slice(0, maxWidth - 3) + "...";
179
+ }
180
+ function isNumericCell(value) {
181
+ return /^-?\d[\d,]*(\.\d+)?%?$/.test(value.trim());
157
182
  }
158
183
  function cellValue(val) {
159
184
  if (val === null || val === void 0) return "";
@@ -170,12 +195,29 @@ function formatTable(data) {
170
195
  if (!firstRow) return "";
171
196
  const keys = Object.keys(firstRow);
172
197
  if (keys.length === 0) return "";
198
+ const colCount = keys.length;
199
+ const maxCellWidth = computeMaxCellWidth(colCount);
173
200
  const widths = keys.map(
174
- (key) => Math.max(key.length, ...rows.map((row) => truncateCell(cellValue(row[key])).length))
201
+ (key) => Math.max(
202
+ key.length,
203
+ ...rows.map((row) => truncateCell(cellValue(row[key]), maxCellWidth).length)
204
+ )
205
+ );
206
+ const isNumeric = keys.map(
207
+ (key) => rows.every((row) => {
208
+ const v = cellValue(row[key]);
209
+ return v === "" || isNumericCell(v);
210
+ })
175
211
  );
176
- const header = keys.map((key, i) => key.padEnd(widths[i] ?? 0)).join(" ");
177
- const separator = widths.map((w) => "-".repeat(w)).join(" ");
178
- const body = rows.map((row) => keys.map((key, i) => truncateCell(cellValue(row[key])).padEnd(widths[i] ?? 0)).join(" ")).join("\n");
212
+ const header = keys.map((key, i) => bold(key.padEnd(widths[i] ?? 0))).join(" ");
213
+ const separator = widths.map((w) => dim("\u2500".repeat(w))).join(" ");
214
+ const body = rows.map(
215
+ (row) => keys.map((key, i) => {
216
+ const cell = truncateCell(cellValue(row[key]), maxCellWidth);
217
+ const w = widths[i] ?? 0;
218
+ return isNumeric[i] ? cell.padStart(w) : cell.padEnd(w);
219
+ }).join(" ")
220
+ ).join("\n");
179
221
  return `${header}
180
222
  ${separator}
181
223
  ${body}`;
@@ -284,6 +326,28 @@ function buildTestCase(item, commandName, index = 0) {
284
326
  failed: false
285
327
  };
286
328
  }
329
+ async function maybePaginate(output) {
330
+ const isTTY = process2.stdout.isTTY;
331
+ const termHeight = process2.stdout.rows ?? 24;
332
+ const lineCount = output.split("\n").length;
333
+ if (!isTTY || lineCount <= termHeight) {
334
+ console.log(output);
335
+ return;
336
+ }
337
+ const pager = process2.env["GPC_PAGER"] ?? process2.env["PAGER"] ?? "less";
338
+ try {
339
+ const { spawn } = await import("child_process");
340
+ const child = spawn(pager, [], {
341
+ stdio: ["pipe", "inherit", "inherit"],
342
+ env: { ...process2.env, LESS: process2.env["LESS"] ?? "-FRX" }
343
+ });
344
+ child.stdin.write(output);
345
+ child.stdin.end();
346
+ await new Promise((resolve2) => child.on("close", resolve2));
347
+ } catch {
348
+ console.log(output);
349
+ }
350
+ }
287
351
  function formatJunit(data, commandName = "command") {
288
352
  const { cases, failures } = toTestCases(data, commandName);
289
353
  const tests = cases.length;
@@ -496,6 +560,7 @@ async function getAppInfo(client, packageName) {
496
560
 
497
561
  // src/commands/releases.ts
498
562
  import { stat as stat2 } from "fs/promises";
563
+ import { ApiError as ApiError2 } from "@gpc-cli/api";
499
564
 
500
565
  // src/utils/file-validation.ts
501
566
  import { readFile, stat } from "fs/promises";
@@ -745,12 +810,13 @@ async function updateRollout(client, packageName, track, action, userFraction) {
745
810
  let newFraction;
746
811
  switch (action) {
747
812
  case "increase":
748
- if (!userFraction) throw new GpcError(
749
- "--to <percentage> is required for rollout increase",
750
- "ROLLOUT_MISSING_FRACTION",
751
- 2,
752
- "Specify the target rollout percentage with --to, e.g.: gpc rollout increase --to 0.5"
753
- );
813
+ if (!userFraction)
814
+ throw new GpcError(
815
+ "--to <percentage> is required for rollout increase",
816
+ "ROLLOUT_MISSING_FRACTION",
817
+ 2,
818
+ "Specify the target rollout percentage with --to, e.g.: gpc rollout increase --to 0.5"
819
+ );
754
820
  if (userFraction <= 0 || userFraction > 1) {
755
821
  throw new GpcError(
756
822
  "Rollout percentage must be between 0 and 1 (e.g., 0.1 for 10%)",
@@ -1161,6 +1227,70 @@ function diffListings(local, remote) {
1161
1227
  return diffs;
1162
1228
  }
1163
1229
 
1230
+ // src/utils/listing-text.ts
1231
+ var DEFAULT_LIMITS = {
1232
+ title: 30,
1233
+ shortDescription: 80,
1234
+ fullDescription: 4e3,
1235
+ video: 256
1236
+ };
1237
+ function lintListing(language, fields, limits = DEFAULT_LIMITS) {
1238
+ const fieldResults = [];
1239
+ for (const [field, limit] of Object.entries(limits)) {
1240
+ const value = fields[field] ?? "";
1241
+ const chars = [...value].length;
1242
+ const pct = Math.round(chars / limit * 100);
1243
+ let status = "ok";
1244
+ if (chars > limit) status = "over";
1245
+ else if (pct >= 80) status = "warn";
1246
+ fieldResults.push({ field, chars, limit, pct, status });
1247
+ }
1248
+ const valid = fieldResults.every((r) => r.status !== "over");
1249
+ return { language, fields: fieldResults, valid };
1250
+ }
1251
+ function lintListings(listings, limits) {
1252
+ return listings.map((l) => lintListing(l.language, l.fields, limits));
1253
+ }
1254
+ function tokenize(text) {
1255
+ return text.split(/(\s+)/);
1256
+ }
1257
+ function wordDiff(before, after) {
1258
+ const aTokens = tokenize(before);
1259
+ const bTokens = tokenize(after);
1260
+ const m = aTokens.length;
1261
+ const n = bTokens.length;
1262
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
1263
+ const cell = (r, c) => dp[r]?.[c] ?? 0;
1264
+ for (let i2 = 1; i2 <= m; i2++) {
1265
+ for (let j2 = 1; j2 <= n; j2++) {
1266
+ dp[i2][j2] = aTokens[i2 - 1] === bTokens[j2 - 1] ? cell(i2 - 1, j2 - 1) + 1 : Math.max(cell(i2 - 1, j2), cell(i2, j2 - 1));
1267
+ }
1268
+ }
1269
+ const result = [];
1270
+ let i = m, j = n;
1271
+ while (i > 0 || j > 0) {
1272
+ if (i > 0 && j > 0 && aTokens[i - 1] === bTokens[j - 1]) {
1273
+ result.unshift({ text: aTokens[i - 1] ?? "", type: "equal" });
1274
+ i--;
1275
+ j--;
1276
+ } else if (j > 0 && (i === 0 || cell(i, j - 1) >= cell(i - 1, j))) {
1277
+ result.unshift({ text: bTokens[j - 1] ?? "", type: "insert" });
1278
+ j--;
1279
+ } else {
1280
+ result.unshift({ text: aTokens[i - 1] ?? "", type: "delete" });
1281
+ i--;
1282
+ }
1283
+ }
1284
+ return result;
1285
+ }
1286
+ function formatWordDiff(diff) {
1287
+ return diff.map((t) => {
1288
+ if (t.type === "equal") return t.text;
1289
+ if (t.type === "insert") return `[+${t.text}]`;
1290
+ return `[-${t.text}]`;
1291
+ }).join("");
1292
+ }
1293
+
1164
1294
  // src/commands/listings.ts
1165
1295
  function validateLanguage(lang) {
1166
1296
  if (!isValidBcp47(lang)) {
@@ -1231,6 +1361,57 @@ async function pullListings(client, packageName, dir) {
1231
1361
  throw error;
1232
1362
  }
1233
1363
  }
1364
+ async function lintLocalListings(dir) {
1365
+ const localListings = await readListingsFromDir(dir);
1366
+ return lintListings(
1367
+ localListings.map((l) => ({
1368
+ language: l.language,
1369
+ fields: {
1370
+ title: l.title,
1371
+ shortDescription: l.shortDescription,
1372
+ fullDescription: l.fullDescription,
1373
+ video: l.video
1374
+ }
1375
+ }))
1376
+ );
1377
+ }
1378
+ async function analyzeRemoteListings(client, packageName, options) {
1379
+ const listings = await getListings(client, packageName);
1380
+ const results = lintListings(
1381
+ listings.map((l) => ({
1382
+ language: l.language,
1383
+ fields: {
1384
+ title: l.title,
1385
+ shortDescription: l.shortDescription,
1386
+ fullDescription: l.fullDescription,
1387
+ video: l["video"]
1388
+ }
1389
+ }))
1390
+ );
1391
+ let missingLocales;
1392
+ if (options?.expectedLocales) {
1393
+ const present = new Set(listings.map((l) => l.language));
1394
+ missingLocales = options.expectedLocales.filter((loc) => !present.has(loc));
1395
+ }
1396
+ return { results, missingLocales };
1397
+ }
1398
+ async function diffListingsEnhanced(client, packageName, dir, options) {
1399
+ const allDiffs = await diffListingsCommand(client, packageName, dir);
1400
+ let result = allDiffs;
1401
+ if (options?.lang) {
1402
+ result = allDiffs.filter((d) => d.language === options.lang);
1403
+ }
1404
+ if (options?.wordLevel) {
1405
+ return result.map((d) => {
1406
+ if (d.field === "fullDescription" && d.local && d.remote) {
1407
+ const diff = wordDiff(d.remote, d.local);
1408
+ return { ...d, diffSummary: formatWordDiff(diff) };
1409
+ }
1410
+ return d;
1411
+ });
1412
+ }
1413
+ return result;
1414
+ }
1234
1415
  async function pushListings(client, packageName, dir, options) {
1235
1416
  const localListings = await readListingsFromDir(dir);
1236
1417
  if (localListings.length === 0) {
@@ -1244,6 +1425,33 @@ async function pushListings(client, packageName, dir, options) {
1244
1425
  for (const listing of localListings) {
1245
1426
  validateLanguage(listing.language);
1246
1427
  }
1428
+ if (!options?.force) {
1429
+ const lintResults = lintListings(
1430
+ localListings.map((l) => ({
1431
+ language: l.language,
1432
+ fields: {
1433
+ title: l.title,
1434
+ shortDescription: l.shortDescription,
1435
+ fullDescription: l.fullDescription,
1436
+ video: l["video"]
1437
+ }
1438
+ }))
1439
+ );
1440
+ const overLimit = lintResults.filter((r) => !r.valid);
1441
+ if (overLimit.length > 0) {
1442
+ const details = overLimit.map((r) => {
1443
+ const over = r.fields.filter((f) => f.status === "over");
1444
+ return `${r.language}: ${over.map((f) => `${f.field} (${f.chars}/${f.limit})`).join(", ")}`;
1445
+ }).join("\n");
1446
+ throw new GpcError(
1447
+ `Listing push blocked: field(s) exceed character limits:
1448
+ ${details}`,
1449
+ "LISTING_CHAR_LIMIT_EXCEEDED",
1450
+ 1,
1451
+ "Fix the character limit violations listed above, or use --force to push anyway."
1452
+ );
1453
+ }
1454
+ }
1247
1455
  const edit = await client.edits.insert(packageName);
1248
1456
  try {
1249
1457
  if (options?.dryRun) {
@@ -1358,8 +1566,8 @@ var ALL_IMAGE_TYPES = [
1358
1566
  "tvBanner"
1359
1567
  ];
1360
1568
  async function exportImages(client, packageName, dir, options) {
1361
- const { mkdir: mkdir6, writeFile: writeFile8 } = await import("fs/promises");
1362
- const { join: join8 } = await import("path");
1569
+ const { mkdir: mkdir7, writeFile: writeFile9 } = await import("fs/promises");
1570
+ const { join: join9 } = await import("path");
1363
1571
  const edit = await client.edits.insert(packageName);
1364
1572
  try {
1365
1573
  let languages;
@@ -1390,12 +1598,12 @@ async function exportImages(client, packageName, dir, options) {
1390
1598
  const batch = tasks.slice(i, i + concurrency);
1391
1599
  const results = await Promise.all(
1392
1600
  batch.map(async (task) => {
1393
- const dirPath = join8(dir, task.language, task.imageType);
1394
- await mkdir6(dirPath, { recursive: true });
1601
+ const dirPath = join9(dir, task.language, task.imageType);
1602
+ await mkdir7(dirPath, { recursive: true });
1395
1603
  const response = await fetch(task.url);
1396
1604
  const buffer = Buffer.from(await response.arrayBuffer());
1397
- const filePath = join8(dirPath, `${task.index}.png`);
1398
- await writeFile8(filePath, buffer);
1605
+ const filePath = join9(dirPath, `${task.index}.png`);
1606
+ await writeFile9(filePath, buffer);
1399
1607
  return buffer.length;
1400
1608
  })
1401
1609
  );
@@ -1565,9 +1773,7 @@ function generateMigrationPlan(detection) {
1565
1773
  }
1566
1774
  for (const lane of detection.lanes) {
1567
1775
  if (lane.gpcEquivalent) {
1568
- checklist.push(
1569
- `Replace Fastlane lane "${lane.name}" with: ${lane.gpcEquivalent} <your.aab>`
1570
- );
1776
+ checklist.push(`Replace Fastlane lane "${lane.name}" with: ${lane.gpcEquivalent} <your.aab>`);
1571
1777
  }
1572
1778
  if (lane.actions.includes("capture_android_screenshots")) {
1573
1779
  warnings.push(
@@ -1642,7 +1848,9 @@ async function writeMigrationOutput(result, dir) {
1642
1848
  lines.push("| `supply(skip_upload_aab: true)` | `gpc listings push` |");
1643
1849
  lines.push("| `capture_android_screenshots` | No equivalent \u2014 use separate tool |");
1644
1850
  lines.push("");
1645
- lines.push("See the full migration guide: https://yasserstudio.github.io/gpc/migration/from-fastlane");
1851
+ lines.push(
1852
+ "See the full migration guide: https://yasserstudio.github.io/gpc/migration/from-fastlane"
1853
+ );
1646
1854
  lines.push("");
1647
1855
  await writeFile2(migrationPath, lines.join("\n"), "utf-8");
1648
1856
  files.push(migrationPath);
@@ -1898,6 +2106,234 @@ async function publish(client, packageName, filePath, options) {
1898
2106
 
1899
2107
  // src/commands/reviews.ts
1900
2108
  import { paginateAll } from "@gpc-cli/api";
2109
+
2110
+ // src/utils/sentiment.ts
2111
+ var POSITIVE_WORDS = /* @__PURE__ */ new Set([
2112
+ "great",
2113
+ "excellent",
2114
+ "amazing",
2115
+ "awesome",
2116
+ "fantastic",
2117
+ "love",
2118
+ "good",
2119
+ "best",
2120
+ "perfect",
2121
+ "wonderful",
2122
+ "helpful",
2123
+ "easy",
2124
+ "fast",
2125
+ "smooth",
2126
+ "reliable",
2127
+ "clean",
2128
+ "beautiful",
2129
+ "intuitive",
2130
+ "works",
2131
+ "recommend",
2132
+ "useful",
2133
+ "thank",
2134
+ "thanks",
2135
+ "brilliant",
2136
+ "superb",
2137
+ "flawless",
2138
+ "outstanding",
2139
+ "delightful",
2140
+ "nice"
2141
+ ]);
2142
+ var NEGATIVE_WORDS = /* @__PURE__ */ new Set([
2143
+ "bad",
2144
+ "terrible",
2145
+ "awful",
2146
+ "horrible",
2147
+ "worst",
2148
+ "hate",
2149
+ "broken",
2150
+ "crash",
2151
+ "crashes",
2152
+ "bug",
2153
+ "bugs",
2154
+ "slow",
2155
+ "laggy",
2156
+ "freeze",
2157
+ "freezes",
2158
+ "error",
2159
+ "errors",
2160
+ "fail",
2161
+ "fails",
2162
+ "useless",
2163
+ "disappointing",
2164
+ "disappointed",
2165
+ "frustrating",
2166
+ "frustration",
2167
+ "annoying",
2168
+ "problem",
2169
+ "problems",
2170
+ "issue",
2171
+ "issues",
2172
+ "fix",
2173
+ "please",
2174
+ "not working",
2175
+ "doesn't work",
2176
+ "stopped",
2177
+ "uninstall",
2178
+ "deleted",
2179
+ "waste",
2180
+ "rubbish",
2181
+ "garbage",
2182
+ "terrible"
2183
+ ]);
2184
+ var STOP_WORDS = /* @__PURE__ */ new Set([
2185
+ "the",
2186
+ "a",
2187
+ "an",
2188
+ "and",
2189
+ "or",
2190
+ "but",
2191
+ "in",
2192
+ "on",
2193
+ "at",
2194
+ "to",
2195
+ "for",
2196
+ "of",
2197
+ "with",
2198
+ "is",
2199
+ "it",
2200
+ "this",
2201
+ "that",
2202
+ "was",
2203
+ "are",
2204
+ "be",
2205
+ "been",
2206
+ "have",
2207
+ "has",
2208
+ "had",
2209
+ "do",
2210
+ "does",
2211
+ "did",
2212
+ "will",
2213
+ "would",
2214
+ "could",
2215
+ "should",
2216
+ "may",
2217
+ "might",
2218
+ "i",
2219
+ "me",
2220
+ "my",
2221
+ "we",
2222
+ "you",
2223
+ "he",
2224
+ "she",
2225
+ "they",
2226
+ "them",
2227
+ "their",
2228
+ "its",
2229
+ "not",
2230
+ "no",
2231
+ "very",
2232
+ "so",
2233
+ "just",
2234
+ "really",
2235
+ "app",
2236
+ "application",
2237
+ "update"
2238
+ ]);
2239
+ function analyzeSentiment(text) {
2240
+ const lower = text.toLowerCase();
2241
+ const words = lower.split(/\W+/).filter(Boolean);
2242
+ let posScore = 0;
2243
+ let negScore = 0;
2244
+ for (const word of words) {
2245
+ if (POSITIVE_WORDS.has(word)) posScore++;
2246
+ if (NEGATIVE_WORDS.has(word)) negScore++;
2247
+ }
2248
+ const total = posScore + negScore;
2249
+ if (total === 0) return { score: 0, label: "neutral", magnitude: 0 };
2250
+ const score = (posScore - negScore) / total;
2251
+ const magnitude = Math.min(1, total / 10);
2252
+ const label = score > 0.1 ? "positive" : score < -0.1 ? "negative" : "neutral";
2253
+ return { score, label, magnitude };
2254
+ }
2255
+ function clusterTopics(texts) {
2256
+ const TOPIC_KEYWORDS = {
2257
+ performance: ["slow", "lag", "laggy", "freeze", "fast", "speed", "quick", "smooth"],
2258
+ crashes: ["crash", "crashes", "crash", "crashing", "force close", "stops", "stopped"],
2259
+ "ui/ux": ["ui", "design", "interface", "layout", "button", "screen", "menu", "navigation"],
2260
+ battery: ["battery", "drain", "power", "charging", "drain"],
2261
+ updates: ["update", "updated", "version", "new version", "after update"],
2262
+ notifications: ["notification", "notifications", "alert", "alerts", "push"],
2263
+ "login/auth": ["login", "sign in", "logout", "password", "account", "auth"],
2264
+ "feature requests": ["please add", "would be nice", "missing", "need", "wish", "want"],
2265
+ bugs: ["bug", "bugs", "issue", "error", "problem", "glitch", "broken"],
2266
+ pricing: ["price", "pricing", "expensive", "cheap", "subscription", "pay", "cost", "free"]
2267
+ };
2268
+ const clusterMap = /* @__PURE__ */ new Map();
2269
+ for (const text of texts) {
2270
+ const lower = text.toLowerCase();
2271
+ const sentiment = analyzeSentiment(text);
2272
+ for (const [topic, keywords] of Object.entries(TOPIC_KEYWORDS)) {
2273
+ if (keywords.some((kw) => lower.includes(kw))) {
2274
+ const existing = clusterMap.get(topic) ?? { count: 0, totalScore: 0 };
2275
+ clusterMap.set(topic, {
2276
+ count: existing.count + 1,
2277
+ totalScore: existing.totalScore + sentiment.score
2278
+ });
2279
+ }
2280
+ }
2281
+ }
2282
+ return Array.from(clusterMap.entries()).map(([topic, { count, totalScore }]) => ({
2283
+ topic,
2284
+ count,
2285
+ avgScore: count > 0 ? Math.round(totalScore / count * 100) / 100 : 0
2286
+ })).filter((c) => c.count > 0).sort((a, b) => b.count - a.count);
2287
+ }
2288
+ function keywordFrequency(texts, topN = 20) {
2289
+ const freq = /* @__PURE__ */ new Map();
2290
+ for (const text of texts) {
2291
+ const words = text.toLowerCase().split(/\W+/).filter((w) => w.length > 3 && !STOP_WORDS.has(w));
2292
+ for (const word of words) {
2293
+ freq.set(word, (freq.get(word) ?? 0) + 1);
2294
+ }
2295
+ }
2296
+ return Array.from(freq.entries()).map(([word, count]) => ({ word, count })).sort((a, b) => b.count - a.count).slice(0, topN);
2297
+ }
2298
+ function analyzeReviews(reviews) {
2299
+ if (reviews.length === 0) {
2300
+ return {
2301
+ totalReviews: 0,
2302
+ avgRating: 0,
2303
+ sentiment: { positive: 0, negative: 0, neutral: 0, avgScore: 0 },
2304
+ topics: [],
2305
+ keywords: [],
2306
+ ratingDistribution: {}
2307
+ };
2308
+ }
2309
+ const texts = reviews.map((r) => r.text);
2310
+ const sentiments = texts.map((t) => analyzeSentiment(t));
2311
+ const positive = sentiments.filter((s) => s.label === "positive").length;
2312
+ const negative = sentiments.filter((s) => s.label === "negative").length;
2313
+ const neutral = sentiments.filter((s) => s.label === "neutral").length;
2314
+ const avgScore = sentiments.reduce((sum, s) => sum + s.score, 0) / sentiments.length;
2315
+ const ratings = reviews.map((r) => r.rating).filter((r) => r !== void 0);
2316
+ const avgRating = ratings.length > 0 ? ratings.reduce((a, b) => a + b, 0) / ratings.length : 0;
2317
+ const ratingDistribution = {};
2318
+ for (const r of ratings) {
2319
+ ratingDistribution[r] = (ratingDistribution[r] ?? 0) + 1;
2320
+ }
2321
+ return {
2322
+ totalReviews: reviews.length,
2323
+ avgRating: Math.round(avgRating * 100) / 100,
2324
+ sentiment: {
2325
+ positive,
2326
+ negative,
2327
+ neutral,
2328
+ avgScore: Math.round(avgScore * 100) / 100
2329
+ },
2330
+ topics: clusterTopics(texts),
2331
+ keywords: keywordFrequency(texts),
2332
+ ratingDistribution
2333
+ };
2334
+ }
2335
+
2336
+ // src/commands/reviews.ts
1901
2337
  async function listReviews(client, packageName, options) {
1902
2338
  const apiOptions = {};
1903
2339
  if (options?.translationLanguage) apiOptions.translationLanguage = options.translationLanguage;
@@ -2007,163 +2443,16 @@ function csvEscape(value) {
2007
2443
  }
2008
2444
  return value;
2009
2445
  }
2010
-
2011
- // src/commands/vitals.ts
2012
- var METRIC_SET_METRICS = {
2013
- crashRateMetricSet: ["crashRate", "userPerceivedCrashRate", "distinctUsers"],
2014
- anrRateMetricSet: ["anrRate", "userPerceivedAnrRate", "distinctUsers"],
2015
- slowStartRateMetricSet: ["slowStartRate", "distinctUsers"],
2016
- slowRenderingRateMetricSet: ["slowRenderingRate", "distinctUsers"],
2017
- excessiveWakeupRateMetricSet: ["excessiveWakeupRate", "distinctUsers"],
2018
- stuckBackgroundWakelockRateMetricSet: ["stuckBackgroundWakelockRate", "distinctUsers"],
2019
- errorCountMetricSet: ["errorReportCount", "distinctUsers"]
2020
- };
2021
- function buildQuery(metricSet, options) {
2022
- const metrics = METRIC_SET_METRICS[metricSet] ?? ["errorReportCount", "distinctUsers"];
2023
- const days = options?.days ?? 30;
2024
- const DAY_MS = 24 * 60 * 60 * 1e3;
2025
- const end = new Date(Date.now() - DAY_MS);
2026
- const start = new Date(Date.now() - DAY_MS - days * DAY_MS);
2027
- const query = {
2028
- metrics,
2029
- timelineSpec: {
2030
- aggregationPeriod: options?.aggregation ?? "DAILY",
2031
- startTime: {
2032
- year: start.getUTCFullYear(),
2033
- month: start.getUTCMonth() + 1,
2034
- day: start.getUTCDate()
2035
- },
2036
- endTime: {
2037
- year: end.getUTCFullYear(),
2038
- month: end.getUTCMonth() + 1,
2039
- day: end.getUTCDate()
2040
- }
2041
- }
2042
- };
2043
- if (options?.dimension) {
2044
- query.dimensions = [options.dimension];
2045
- }
2046
- return query;
2047
- }
2048
- async function queryMetric(reporting, packageName, metricSet, options) {
2049
- const query = buildQuery(metricSet, options);
2050
- return reporting.queryMetricSet(packageName, metricSet, query);
2051
- }
2052
- async function getVitalsOverview(reporting, packageName) {
2053
- const metricSets = [
2054
- ["crashRateMetricSet", "crashRate"],
2055
- ["anrRateMetricSet", "anrRate"],
2056
- ["slowStartRateMetricSet", "slowStartRate"],
2057
- ["slowRenderingRateMetricSet", "slowRenderingRate"],
2058
- ["excessiveWakeupRateMetricSet", "excessiveWakeupRate"],
2059
- ["stuckBackgroundWakelockRateMetricSet", "stuckWakelockRate"]
2060
- ];
2061
- const results = await Promise.allSettled(
2062
- metricSets.map(
2063
- ([metric]) => reporting.queryMetricSet(packageName, metric, buildQuery(metric))
2064
- )
2065
- );
2066
- const overview = {};
2067
- for (let i = 0; i < metricSets.length; i++) {
2068
- const entry = metricSets[i];
2069
- if (!entry) continue;
2070
- const key = entry[1];
2071
- const result = results[i];
2072
- if (!result) continue;
2073
- if (result.status === "fulfilled") {
2074
- overview[key] = result.value.rows || [];
2075
- }
2076
- }
2077
- return overview;
2078
- }
2079
- async function getVitalsCrashes(reporting, packageName, options) {
2080
- return queryMetric(reporting, packageName, "crashRateMetricSet", options);
2081
- }
2082
- async function getVitalsAnr(reporting, packageName, options) {
2083
- return queryMetric(reporting, packageName, "anrRateMetricSet", options);
2084
- }
2085
- async function getVitalsStartup(reporting, packageName, options) {
2086
- return queryMetric(reporting, packageName, "slowStartRateMetricSet", options);
2087
- }
2088
- async function getVitalsRendering(reporting, packageName, options) {
2089
- return queryMetric(reporting, packageName, "slowRenderingRateMetricSet", options);
2090
- }
2091
- async function getVitalsBattery(reporting, packageName, options) {
2092
- return queryMetric(reporting, packageName, "excessiveWakeupRateMetricSet", options);
2093
- }
2094
- async function getVitalsMemory(reporting, packageName, options) {
2095
- return queryMetric(reporting, packageName, "stuckBackgroundWakelockRateMetricSet", options);
2096
- }
2097
- async function getVitalsAnomalies(reporting, packageName) {
2098
- return reporting.getAnomalies(packageName);
2099
- }
2100
- async function searchVitalsErrors(reporting, packageName, options) {
2101
- return reporting.searchErrorIssues(packageName, options?.filter, options?.maxResults);
2102
- }
2103
- async function compareVitalsTrend(reporting, packageName, metricSet, days = 7) {
2104
- const DAY_MS = 24 * 60 * 60 * 1e3;
2105
- const nowMs = Date.now();
2106
- const baseMs = nowMs - 2 * DAY_MS;
2107
- const currentEnd = new Date(baseMs);
2108
- const currentStart = new Date(baseMs - days * DAY_MS);
2109
- const previousEnd = new Date(baseMs - days * DAY_MS - DAY_MS);
2110
- const previousStart = new Date(baseMs - days * DAY_MS - DAY_MS - days * DAY_MS);
2111
- const metrics = METRIC_SET_METRICS[metricSet] ?? ["errorReportCount", "distinctUsers"];
2112
- const toApiDate2 = (d) => ({
2113
- year: d.getUTCFullYear(),
2114
- month: d.getUTCMonth() + 1,
2115
- day: d.getUTCDate()
2116
- });
2117
- const makeQuery = (start, end) => ({
2118
- metrics,
2119
- timelineSpec: {
2120
- aggregationPeriod: "DAILY",
2121
- startTime: toApiDate2(start),
2122
- endTime: toApiDate2(end)
2123
- }
2446
+ async function analyzeReviews2(client, packageName, options) {
2447
+ const reviews = await listReviews(client, packageName, options);
2448
+ const items = reviews.map((r) => {
2449
+ const uc = r.comments?.[0]?.userComment;
2450
+ return {
2451
+ text: uc?.text ?? "",
2452
+ rating: uc?.starRating
2453
+ };
2124
2454
  });
2125
- const [currentResult, previousResult] = await Promise.all([
2126
- reporting.queryMetricSet(packageName, metricSet, makeQuery(currentStart, currentEnd)),
2127
- reporting.queryMetricSet(packageName, metricSet, makeQuery(previousStart, previousEnd))
2128
- ]);
2129
- const extractAvg = (rows) => {
2130
- if (!rows || rows.length === 0) return void 0;
2131
- const values = rows.map((r) => {
2132
- const keys = Object.keys(r.metrics);
2133
- const first = keys[0];
2134
- return first ? Number(r.metrics[first]?.decimalValue?.value) : NaN;
2135
- }).filter((v) => !isNaN(v));
2136
- if (values.length === 0) return void 0;
2137
- return values.reduce((a, b) => a + b, 0) / values.length;
2138
- };
2139
- const current = extractAvg(currentResult.rows);
2140
- const previous = extractAvg(previousResult.rows);
2141
- let changePercent;
2142
- let direction = "unknown";
2143
- if (current !== void 0 && previous !== void 0 && previous !== 0) {
2144
- changePercent = (current - previous) / previous * 100;
2145
- if (Math.abs(changePercent) < 1) {
2146
- direction = "unchanged";
2147
- } else if (changePercent < 0) {
2148
- direction = "improved";
2149
- } else {
2150
- direction = "degraded";
2151
- }
2152
- }
2153
- return {
2154
- metric: metricSet,
2155
- current,
2156
- previous,
2157
- changePercent: changePercent !== void 0 ? Math.round(changePercent * 10) / 10 : void 0,
2158
- direction
2159
- };
2160
- }
2161
- function checkThreshold(value, threshold) {
2162
- return {
2163
- breached: value !== void 0 && value > threshold,
2164
- value,
2165
- threshold
2166
- };
2455
+ return analyzeReviews(items);
2167
2456
  }
2168
2457
 
2169
2458
  // src/commands/subscriptions.ts
@@ -2180,7 +2469,9 @@ function sanitizeSubscription(data) {
2180
2469
  delete cleaned["archived"];
2181
2470
  if (cleaned.basePlans) {
2182
2471
  cleaned.basePlans = cleaned.basePlans.map((bp) => {
2183
- const { state: _s, archived: _a, ...cleanBp } = bp;
2472
+ const { state: _state, archived: _archived, ...cleanBp } = bp;
2473
+ void _state;
2474
+ void _archived;
2184
2475
  if (cleanBp.regionalConfigs) {
2185
2476
  cleanBp.regionalConfigs = cleanBp.regionalConfigs.map((rc) => ({
2186
2477
  ...rc,
@@ -2193,7 +2484,8 @@ function sanitizeSubscription(data) {
2193
2484
  return cleaned;
2194
2485
  }
2195
2486
  function sanitizeOffer(data) {
2196
- const { state: _s, ...cleaned } = data;
2487
+ const { state: _state2, ...cleaned } = data;
2488
+ void _state2;
2197
2489
  delete cleaned["archived"];
2198
2490
  return cleaned;
2199
2491
  }
@@ -2211,7 +2503,8 @@ function autoFixProrationMode(data) {
2211
2503
  for (const bp of data.basePlans) {
2212
2504
  const mode = bp.autoRenewingBasePlanType?.prorationMode;
2213
2505
  if (mode && !mode.startsWith(PRORATION_MODE_PREFIX)) {
2214
- bp.autoRenewingBasePlanType.prorationMode = `${PRORATION_MODE_PREFIX}${mode}`;
2506
+ if (bp.autoRenewingBasePlanType)
2507
+ bp.autoRenewingBasePlanType.prorationMode = `${PRORATION_MODE_PREFIX}${mode}`;
2215
2508
  }
2216
2509
  if (bp.autoRenewingBasePlanType?.prorationMode) {
2217
2510
  const fullMode = bp.autoRenewingBasePlanType.prorationMode;
@@ -2349,7 +2642,13 @@ async function createOffer(client, packageName, productId, basePlanId, data) {
2349
2642
  validatePackageName(packageName);
2350
2643
  validateSku(productId);
2351
2644
  const sanitized = sanitizeOffer(data);
2352
- return client.subscriptions.createOffer(packageName, productId, basePlanId, sanitized, data.offerId);
2645
+ return client.subscriptions.createOffer(
2646
+ packageName,
2647
+ productId,
2648
+ basePlanId,
2649
+ sanitized,
2650
+ data.offerId
2651
+ );
2353
2652
  }
2354
2653
  var OFFER_ID_FIELDS = /* @__PURE__ */ new Set(["productId", "basePlanId", "offerId"]);
2355
2654
  function deriveOfferUpdateMask(data) {
@@ -2386,7 +2685,9 @@ async function diffSubscription(client, packageName, productId, localData) {
2386
2685
  const diffs = [];
2387
2686
  const fieldsToCompare = ["listings", "basePlans", "taxAndComplianceSettings"];
2388
2687
  for (const field of fieldsToCompare) {
2389
- const localVal = JSON.stringify(localData[field] ?? null);
2688
+ const localVal = JSON.stringify(
2689
+ localData[field] ?? null
2690
+ );
2390
2691
  const remoteVal = JSON.stringify(remote[field] ?? null);
2391
2692
  if (localVal !== remoteVal) {
2392
2693
  diffs.push({ field, local: localVal, remote: remoteVal });
@@ -2399,20 +2700,331 @@ async function deactivateOffer(client, packageName, productId, basePlanId, offer
2399
2700
  validateSku(productId);
2400
2701
  return client.subscriptions.deactivateOffer(packageName, productId, basePlanId, offerId);
2401
2702
  }
2402
-
2403
- // src/commands/iap.ts
2404
- import { readdir as readdir4, readFile as readFile5 } from "fs/promises";
2405
- import { join as join4 } from "path";
2406
- import { paginateAll as paginateAll3 } from "@gpc-cli/api";
2407
- async function listInAppProducts(client, packageName, options) {
2408
- if (options?.limit || options?.nextPage) {
2409
- const result = await paginateAll3(
2410
- async (pageToken) => {
2411
- const resp = await client.inappproducts.list(packageName, {
2412
- token: pageToken,
2413
- maxResults: options?.maxResults
2414
- });
2415
- return {
2703
+ async function getSubscriptionAnalytics(client, packageName) {
2704
+ validatePackageName(packageName);
2705
+ const { items: subs } = await paginateAll2(async (pageToken) => {
2706
+ const response = await client.subscriptions.list(packageName, {
2707
+ pageToken,
2708
+ pageSize: 100
2709
+ });
2710
+ return {
2711
+ items: response.subscriptions || [],
2712
+ nextPageToken: response.nextPageToken
2713
+ };
2714
+ });
2715
+ let activeCount = 0;
2716
+ let activeBasePlans = 0;
2717
+ let trialBasePlans = 0;
2718
+ let pausedBasePlans = 0;
2719
+ let canceledBasePlans = 0;
2720
+ let totalOffers = 0;
2721
+ const byProductId = [];
2722
+ for (const sub of subs) {
2723
+ const state = sub["state"];
2724
+ if (state === "ACTIVE") activeCount++;
2725
+ const basePlans = sub.basePlans ?? [];
2726
+ let subOfferCount = 0;
2727
+ for (const bp of basePlans) {
2728
+ const bpState = bp["state"];
2729
+ if (bpState === "ACTIVE") activeBasePlans++;
2730
+ else if (bpState === "DRAFT") trialBasePlans++;
2731
+ else if (bpState === "INACTIVE") pausedBasePlans++;
2732
+ else if (bpState === "PREPUBLISHED") canceledBasePlans++;
2733
+ }
2734
+ for (const bp of basePlans) {
2735
+ try {
2736
+ const offersResp = await client.subscriptions.listOffers(
2737
+ packageName,
2738
+ sub.productId,
2739
+ bp.basePlanId
2740
+ );
2741
+ const offers = offersResp.subscriptionOffers ?? [];
2742
+ subOfferCount += offers.length;
2743
+ totalOffers += offers.length;
2744
+ } catch {
2745
+ }
2746
+ }
2747
+ byProductId.push({
2748
+ productId: sub.productId,
2749
+ state: sub["state"] ?? "UNKNOWN",
2750
+ basePlanCount: basePlans.length,
2751
+ offerCount: subOfferCount
2752
+ });
2753
+ }
2754
+ return {
2755
+ totalSubscriptions: subs.length,
2756
+ activeCount,
2757
+ activeBasePlans,
2758
+ trialBasePlans,
2759
+ pausedBasePlans,
2760
+ canceledBasePlans,
2761
+ offerCount: totalOffers,
2762
+ byProductId
2763
+ };
2764
+ }
2765
+
2766
+ // src/commands/vitals.ts
2767
+ var METRIC_SET_METRICS = {
2768
+ crashRateMetricSet: ["crashRate", "userPerceivedCrashRate", "distinctUsers"],
2769
+ anrRateMetricSet: ["anrRate", "userPerceivedAnrRate", "distinctUsers"],
2770
+ slowStartRateMetricSet: ["slowStartRate", "distinctUsers"],
2771
+ slowRenderingRateMetricSet: ["slowRenderingRate", "distinctUsers"],
2772
+ excessiveWakeupRateMetricSet: ["excessiveWakeupRate", "distinctUsers"],
2773
+ // API requires the weighted variants — base `stuckBackgroundWakelockRate` is not a valid metric
2774
+ stuckBackgroundWakelockRateMetricSet: [
2775
+ "stuckBackgroundWakelockRate7dUserWeighted",
2776
+ "stuckBackgroundWakelockRate28dUserWeighted",
2777
+ "distinctUsers"
2778
+ ],
2779
+ errorCountMetricSet: ["errorReportCount", "distinctUsers"]
2780
+ };
2781
+ function buildQuery(metricSet, options) {
2782
+ const metrics = METRIC_SET_METRICS[metricSet] ?? ["errorReportCount", "distinctUsers"];
2783
+ const days = options?.days ?? 30;
2784
+ const DAY_MS = 24 * 60 * 60 * 1e3;
2785
+ const end = new Date(Date.now() - DAY_MS);
2786
+ const start = new Date(Date.now() - DAY_MS - days * DAY_MS);
2787
+ const query = {
2788
+ metrics,
2789
+ timelineSpec: {
2790
+ aggregationPeriod: options?.aggregation ?? "DAILY",
2791
+ startTime: {
2792
+ year: start.getUTCFullYear(),
2793
+ month: start.getUTCMonth() + 1,
2794
+ day: start.getUTCDate()
2795
+ },
2796
+ endTime: {
2797
+ year: end.getUTCFullYear(),
2798
+ month: end.getUTCMonth() + 1,
2799
+ day: end.getUTCDate()
2800
+ }
2801
+ }
2802
+ };
2803
+ if (options?.dimension) {
2804
+ query.dimensions = [options.dimension];
2805
+ }
2806
+ return query;
2807
+ }
2808
+ async function queryMetric(reporting, packageName, metricSet, options) {
2809
+ const query = buildQuery(metricSet, options);
2810
+ return reporting.queryMetricSet(packageName, metricSet, query);
2811
+ }
2812
+ async function getVitalsOverview(reporting, packageName) {
2813
+ const metricSets = [
2814
+ ["crashRateMetricSet", "crashRate"],
2815
+ ["anrRateMetricSet", "anrRate"],
2816
+ ["slowStartRateMetricSet", "slowStartRate"],
2817
+ ["slowRenderingRateMetricSet", "slowRenderingRate"],
2818
+ ["excessiveWakeupRateMetricSet", "excessiveWakeupRate"],
2819
+ ["stuckBackgroundWakelockRateMetricSet", "stuckWakelockRate"]
2820
+ ];
2821
+ const results = await Promise.allSettled(
2822
+ metricSets.map(([metric]) => reporting.queryMetricSet(packageName, metric, buildQuery(metric)))
2823
+ );
2824
+ const overview = {};
2825
+ for (let i = 0; i < metricSets.length; i++) {
2826
+ const entry = metricSets[i];
2827
+ if (!entry) continue;
2828
+ const key = entry[1];
2829
+ const result = results[i];
2830
+ if (!result) continue;
2831
+ if (result.status === "fulfilled") {
2832
+ overview[key] = result.value.rows || [];
2833
+ }
2834
+ }
2835
+ return overview;
2836
+ }
2837
+ async function getVitalsCrashes(reporting, packageName, options) {
2838
+ return queryMetric(reporting, packageName, "crashRateMetricSet", options);
2839
+ }
2840
+ async function getVitalsAnr(reporting, packageName, options) {
2841
+ return queryMetric(reporting, packageName, "anrRateMetricSet", options);
2842
+ }
2843
+ async function getVitalsStartup(reporting, packageName, options) {
2844
+ return queryMetric(reporting, packageName, "slowStartRateMetricSet", options);
2845
+ }
2846
+ async function getVitalsRendering(reporting, packageName, options) {
2847
+ return queryMetric(reporting, packageName, "slowRenderingRateMetricSet", options);
2848
+ }
2849
+ async function getVitalsBattery(reporting, packageName, options) {
2850
+ return queryMetric(reporting, packageName, "excessiveWakeupRateMetricSet", options);
2851
+ }
2852
+ async function getVitalsMemory(reporting, packageName, options) {
2853
+ return queryMetric(reporting, packageName, "stuckBackgroundWakelockRateMetricSet", options);
2854
+ }
2855
+ async function getVitalsLmk(reporting, packageName, options) {
2856
+ return queryMetric(reporting, packageName, "stuckBackgroundWakelockRateMetricSet", {
2857
+ ...options,
2858
+ aggregation: "DAILY"
2859
+ });
2860
+ }
2861
+ async function getVitalsAnomalies(reporting, packageName) {
2862
+ return reporting.getAnomalies(packageName);
2863
+ }
2864
+ async function searchVitalsErrors(reporting, packageName, options) {
2865
+ return reporting.searchErrorIssues(packageName, options?.filter, options?.maxResults);
2866
+ }
2867
+ async function compareVitalsTrend(reporting, packageName, metricSet, days = 7) {
2868
+ const DAY_MS = 24 * 60 * 60 * 1e3;
2869
+ const nowMs = Date.now();
2870
+ const baseMs = nowMs - 2 * DAY_MS;
2871
+ const currentEnd = new Date(baseMs);
2872
+ const currentStart = new Date(baseMs - days * DAY_MS);
2873
+ const previousEnd = new Date(baseMs - days * DAY_MS - DAY_MS);
2874
+ const previousStart = new Date(baseMs - days * DAY_MS - DAY_MS - days * DAY_MS);
2875
+ const metrics = METRIC_SET_METRICS[metricSet] ?? ["errorReportCount", "distinctUsers"];
2876
+ const toApiDate2 = (d) => ({
2877
+ year: d.getUTCFullYear(),
2878
+ month: d.getUTCMonth() + 1,
2879
+ day: d.getUTCDate()
2880
+ });
2881
+ const makeQuery = (start, end) => ({
2882
+ metrics,
2883
+ timelineSpec: {
2884
+ aggregationPeriod: "DAILY",
2885
+ startTime: toApiDate2(start),
2886
+ endTime: toApiDate2(end)
2887
+ }
2888
+ });
2889
+ const [currentResult, previousResult] = await Promise.all([
2890
+ reporting.queryMetricSet(packageName, metricSet, makeQuery(currentStart, currentEnd)),
2891
+ reporting.queryMetricSet(packageName, metricSet, makeQuery(previousStart, previousEnd))
2892
+ ]);
2893
+ const extractAvg = (rows) => {
2894
+ if (!rows || rows.length === 0) return void 0;
2895
+ const values = rows.map((r) => {
2896
+ const keys = Object.keys(r.metrics);
2897
+ const first = keys[0];
2898
+ return first ? Number(r.metrics[first]?.decimalValue?.value) : NaN;
2899
+ }).filter((v) => !isNaN(v));
2900
+ if (values.length === 0) return void 0;
2901
+ return values.reduce((a, b) => a + b, 0) / values.length;
2902
+ };
2903
+ const current = extractAvg(currentResult.rows);
2904
+ const previous = extractAvg(previousResult.rows);
2905
+ let changePercent;
2906
+ let direction = "unknown";
2907
+ if (current !== void 0 && previous !== void 0 && previous !== 0) {
2908
+ changePercent = (current - previous) / previous * 100;
2909
+ if (Math.abs(changePercent) < 1) {
2910
+ direction = "unchanged";
2911
+ } else if (changePercent < 0) {
2912
+ direction = "improved";
2913
+ } else {
2914
+ direction = "degraded";
2915
+ }
2916
+ }
2917
+ return {
2918
+ metric: metricSet,
2919
+ current,
2920
+ previous,
2921
+ changePercent: changePercent !== void 0 ? Math.round(changePercent * 10) / 10 : void 0,
2922
+ direction
2923
+ };
2924
+ }
2925
+ function checkThreshold(value, threshold) {
2926
+ return {
2927
+ breached: value !== void 0 && value > threshold,
2928
+ value,
2929
+ threshold
2930
+ };
2931
+ }
2932
+ async function compareVersionVitals(reporting, packageName, v1, v2, options) {
2933
+ const days = options?.days ?? 30;
2934
+ const metricSets = [
2935
+ ["crashRateMetricSet", "crashRate"],
2936
+ ["anrRateMetricSet", "anrRate"],
2937
+ ["slowStartRateMetricSet", "slowStartRate"],
2938
+ ["slowRenderingRateMetricSet", "slowRenderingRate"]
2939
+ ];
2940
+ const results = await Promise.allSettled(
2941
+ metricSets.map(
2942
+ ([ms]) => queryMetric(reporting, packageName, ms, { dimension: "versionCode", days })
2943
+ )
2944
+ );
2945
+ const row1 = { versionCode: v1 };
2946
+ const row2 = { versionCode: v2 };
2947
+ for (let i = 0; i < metricSets.length; i++) {
2948
+ const entry = metricSets[i];
2949
+ const result = results[i];
2950
+ if (!entry || !result || result.status !== "fulfilled") continue;
2951
+ const key = entry[1];
2952
+ const rows = result.value.rows ?? [];
2953
+ const extractAvgForVersion = (vc) => {
2954
+ const matching = rows.filter((r) => {
2955
+ const dims = r.dimensions;
2956
+ return dims?.some((d) => d["stringValue"] === vc) ?? false;
2957
+ });
2958
+ if (matching.length === 0) return void 0;
2959
+ const values = matching.map((r) => {
2960
+ const firstKey = Object.keys(r.metrics)[0];
2961
+ return firstKey ? Number(r.metrics[firstKey]?.decimalValue?.value) : NaN;
2962
+ }).filter((v) => !isNaN(v));
2963
+ if (values.length === 0) return void 0;
2964
+ return values.reduce((a, b) => a + b, 0) / values.length;
2965
+ };
2966
+ row1[key] = extractAvgForVersion(v1);
2967
+ row2[key] = extractAvgForVersion(v2);
2968
+ }
2969
+ const regressions = [];
2970
+ for (const [, key] of metricSets) {
2971
+ const val1 = row1[key];
2972
+ const val2 = row2[key];
2973
+ if (val1 !== void 0 && val2 !== void 0 && val2 > val1 * 1.05) {
2974
+ regressions.push(key);
2975
+ }
2976
+ }
2977
+ return { v1: row1, v2: row2, regressions };
2978
+ }
2979
+ function watchVitalsWithAutoHalt(reporting, packageName, options) {
2980
+ const {
2981
+ intervalMs = 5 * 60 * 1e3,
2982
+ threshold,
2983
+ metricSet = "crashRateMetricSet",
2984
+ onHalt,
2985
+ onPoll
2986
+ } = options;
2987
+ let stopped = false;
2988
+ let haltTriggered = false;
2989
+ const poll = async () => {
2990
+ if (stopped) return;
2991
+ try {
2992
+ const result = await queryMetric(reporting, packageName, metricSet, { days: 1 });
2993
+ const latestRow = result.rows?.[result.rows.length - 1];
2994
+ const firstMetric = latestRow?.metrics ? Object.keys(latestRow.metrics)[0] : void 0;
2995
+ const value = firstMetric ? Number(latestRow?.metrics[firstMetric]?.decimalValue?.value) : void 0;
2996
+ const breached = value !== void 0 && value > threshold;
2997
+ onPoll?.(value, breached);
2998
+ if (breached && !haltTriggered && onHalt) {
2999
+ haltTriggered = true;
3000
+ await onHalt(value);
3001
+ }
3002
+ } catch {
3003
+ }
3004
+ if (!stopped) {
3005
+ timerId = setTimeout(poll, intervalMs);
3006
+ }
3007
+ };
3008
+ let timerId = setTimeout(poll, 0);
3009
+ return () => {
3010
+ stopped = true;
3011
+ clearTimeout(timerId);
3012
+ };
3013
+ }
3014
+
3015
+ // src/commands/iap.ts
3016
+ import { readdir as readdir4, readFile as readFile5 } from "fs/promises";
3017
+ import { join as join4 } from "path";
3018
+ import { paginateAll as paginateAll3 } from "@gpc-cli/api";
3019
+ async function listInAppProducts(client, packageName, options) {
3020
+ if (options?.limit || options?.nextPage) {
3021
+ const result = await paginateAll3(
3022
+ async (pageToken) => {
3023
+ const resp = await client.inappproducts.list(packageName, {
3024
+ token: pageToken,
3025
+ maxResults: options?.maxResults
3026
+ });
3027
+ return {
2416
3028
  items: resp.inappproduct || [],
2417
3029
  nextPageToken: resp.tokenPagination?.nextPageToken
2418
3030
  };
@@ -2592,6 +3204,10 @@ async function revokeSubscriptionPurchase(client, packageName, token) {
2592
3204
  validatePackageName(packageName);
2593
3205
  return client.purchases.revokeSubscriptionV2(packageName, token);
2594
3206
  }
3207
+ async function refundSubscriptionV2(client, packageName, token) {
3208
+ validatePackageName(packageName);
3209
+ return client.purchases.refundSubscriptionV2(packageName, token);
3210
+ }
2595
3211
  async function listVoidedPurchases(client, packageName, options) {
2596
3212
  validatePackageName(packageName);
2597
3213
  if (options?.limit || options?.nextPage) {
@@ -2666,8 +3282,8 @@ function isStatsReportType(type) {
2666
3282
  function isValidReportType(type) {
2667
3283
  return FINANCIAL_REPORT_TYPES.has(type) || STATS_REPORT_TYPES.has(type);
2668
3284
  }
2669
- function isValidStatsDimension(dim) {
2670
- return VALID_DIMENSIONS.has(dim);
3285
+ function isValidStatsDimension(dim2) {
3286
+ return VALID_DIMENSIONS.has(dim2);
2671
3287
  }
2672
3288
  function parseMonth(monthStr) {
2673
3289
  const match = /^(\d{4})-(\d{2})$/.exec(monthStr);
@@ -2766,6 +3382,32 @@ function parseGrantArg(grantStr) {
2766
3382
  return { packageName, appLevelPermissions: perms };
2767
3383
  }
2768
3384
 
3385
+ // src/commands/grants.ts
3386
+ async function listGrants(client, developerId, email) {
3387
+ const result = await client.grants.list(developerId, email);
3388
+ return result.grants || [];
3389
+ }
3390
+ async function createGrant(client, developerId, email, packageName, permissions) {
3391
+ return client.grants.create(developerId, email, {
3392
+ packageName,
3393
+ appLevelPermissions: permissions
3394
+ });
3395
+ }
3396
+ async function updateGrant(client, developerId, email, packageName, permissions) {
3397
+ return client.grants.patch(
3398
+ developerId,
3399
+ email,
3400
+ packageName,
3401
+ {
3402
+ appLevelPermissions: permissions
3403
+ },
3404
+ "appLevelPermissions"
3405
+ );
3406
+ }
3407
+ async function deleteGrant(client, developerId, email, packageName) {
3408
+ return client.grants.delete(developerId, email, packageName);
3409
+ }
3410
+
2769
3411
  // src/commands/testers.ts
2770
3412
  import { readFile as readFile6 } from "fs/promises";
2771
3413
  async function listTesters(client, packageName, track) {
@@ -2843,7 +3485,7 @@ var DEFAULT_MAX_LENGTH = 500;
2843
3485
  function parseConventionalCommit(subject) {
2844
3486
  const match = subject.match(/^(\w+)(?:\([^)]*\))?:\s*(.+)$/);
2845
3487
  if (match) {
2846
- return { type: match[1], message: match[2].trim() };
3488
+ return { type: match[1] ?? "other", message: (match[2] ?? "").trim() };
2847
3489
  }
2848
3490
  return { type: "other", message: subject.trim() };
2849
3491
  }
@@ -2854,7 +3496,7 @@ function formatNotes(commits, maxLength) {
2854
3496
  if (!groups.has(header)) {
2855
3497
  groups.set(header, []);
2856
3498
  }
2857
- groups.get(header).push(commit.message);
3499
+ groups.get(header)?.push(commit.message);
2858
3500
  }
2859
3501
  const order = ["New", "Fixed", "Improved", "Changes"];
2860
3502
  const sections = [];
@@ -3173,96 +3815,447 @@ async function diffOneTimeProduct(client, packageName, productId, localData) {
3173
3815
  const diffs = [];
3174
3816
  const fieldsToCompare = ["listings", "purchaseType", "taxAndComplianceSettings"];
3175
3817
  for (const field of fieldsToCompare) {
3176
- const localVal = JSON.stringify(localData[field] ?? null);
3818
+ const localVal = JSON.stringify(
3819
+ localData[field] ?? null
3820
+ );
3177
3821
  const remoteVal = JSON.stringify(remote[field] ?? null);
3178
3822
  if (localVal !== remoteVal) {
3179
3823
  diffs.push({ field, local: localVal, remote: remoteVal });
3180
3824
  }
3181
3825
  }
3182
- return diffs;
3183
- }
3184
- async function deleteOneTimeOffer(client, packageName, productId, offerId) {
3185
- try {
3186
- await client.oneTimeProducts.deleteOffer(packageName, productId, offerId);
3187
- } catch (error) {
3188
- throw new GpcError(
3189
- `Failed to delete offer "${offerId}" for product "${productId}": ${error instanceof Error ? error.message : String(error)}`,
3190
- "OTP_OFFER_DELETE_FAILED",
3191
- 4,
3192
- "Check that the product and offer IDs exist."
3193
- );
3826
+ return diffs;
3827
+ }
3828
+ async function deleteOneTimeOffer(client, packageName, productId, offerId) {
3829
+ try {
3830
+ await client.oneTimeProducts.deleteOffer(packageName, productId, offerId);
3831
+ } catch (error) {
3832
+ throw new GpcError(
3833
+ `Failed to delete offer "${offerId}" for product "${productId}": ${error instanceof Error ? error.message : String(error)}`,
3834
+ "OTP_OFFER_DELETE_FAILED",
3835
+ 4,
3836
+ "Check that the product and offer IDs exist."
3837
+ );
3838
+ }
3839
+ }
3840
+
3841
+ // src/utils/spinner.ts
3842
+ import process3 from "process";
3843
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
3844
+ var INTERVAL_MS = 80;
3845
+ function createSpinner(message) {
3846
+ const isTTY = process3.stderr.isTTY === true;
3847
+ let frameIndex = 0;
3848
+ let timer;
3849
+ let currentMessage = message;
3850
+ let started = false;
3851
+ function clearLine() {
3852
+ if (isTTY) {
3853
+ process3.stderr.write("\r\x1B[K");
3854
+ }
3855
+ }
3856
+ function renderFrame() {
3857
+ const frame = FRAMES[frameIndex % FRAMES.length];
3858
+ process3.stderr.write(`\r\x1B[K${frame} ${currentMessage}`);
3859
+ frameIndex++;
3860
+ }
3861
+ return {
3862
+ start() {
3863
+ if (started) return;
3864
+ started = true;
3865
+ if (!isTTY) {
3866
+ process3.stderr.write(`${currentMessage}
3867
+ `);
3868
+ return;
3869
+ }
3870
+ renderFrame();
3871
+ timer = setInterval(renderFrame, INTERVAL_MS);
3872
+ },
3873
+ stop(msg) {
3874
+ if (timer) {
3875
+ clearInterval(timer);
3876
+ timer = void 0;
3877
+ }
3878
+ const text = msg ?? currentMessage;
3879
+ if (isTTY) {
3880
+ clearLine();
3881
+ process3.stderr.write(`\u2714 ${text}
3882
+ `);
3883
+ } else if (!started) {
3884
+ process3.stderr.write(`${text}
3885
+ `);
3886
+ }
3887
+ started = false;
3888
+ },
3889
+ fail(msg) {
3890
+ if (timer) {
3891
+ clearInterval(timer);
3892
+ timer = void 0;
3893
+ }
3894
+ const text = msg ?? currentMessage;
3895
+ if (isTTY) {
3896
+ clearLine();
3897
+ process3.stderr.write(`\u2718 ${text}
3898
+ `);
3899
+ } else if (!started) {
3900
+ process3.stderr.write(`${text}
3901
+ `);
3902
+ }
3903
+ started = false;
3904
+ },
3905
+ update(msg) {
3906
+ currentMessage = msg;
3907
+ if (!isTTY || !started) return;
3908
+ renderFrame();
3909
+ }
3910
+ };
3911
+ }
3912
+
3913
+ // src/utils/train-state.ts
3914
+ import { mkdir as mkdir3, readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
3915
+ import { join as join5 } from "path";
3916
+ import { getCacheDir } from "@gpc-cli/config";
3917
+ function stateFile(packageName) {
3918
+ return join5(getCacheDir(), `train-${packageName}.json`);
3919
+ }
3920
+ async function readTrainState(packageName) {
3921
+ const path = stateFile(packageName);
3922
+ try {
3923
+ const raw = await readFile8(path, "utf-8");
3924
+ return JSON.parse(raw);
3925
+ } catch {
3926
+ return null;
3927
+ }
3928
+ }
3929
+ async function writeTrainState(packageName, state) {
3930
+ const path = stateFile(packageName);
3931
+ const dir = path.substring(0, path.lastIndexOf("/"));
3932
+ await mkdir3(dir, { recursive: true });
3933
+ await writeFile4(path, JSON.stringify(state, null, 2), "utf-8");
3934
+ }
3935
+ async function clearTrainState(packageName) {
3936
+ const { unlink } = await import("fs/promises");
3937
+ const path = stateFile(packageName);
3938
+ await unlink(path).catch(() => {
3939
+ });
3940
+ }
3941
+ function parseDuration2(s) {
3942
+ const match = /^(\d+)(d|h|m)$/.exec(s.trim());
3943
+ if (!match) return 0;
3944
+ const n = parseInt(match[1] ?? "0", 10);
3945
+ switch (match[2]) {
3946
+ case "d":
3947
+ return n * 24 * 60 * 60 * 1e3;
3948
+ case "h":
3949
+ return n * 60 * 60 * 1e3;
3950
+ case "m":
3951
+ return n * 60 * 1e3;
3952
+ default:
3953
+ return 0;
3954
+ }
3955
+ }
3956
+
3957
+ // src/commands/train.ts
3958
+ async function startTrain(apiClient, packageName, config, options) {
3959
+ const existing = await readTrainState(packageName);
3960
+ if (existing && existing.status === "running" && !options?.force) {
3961
+ return existing;
3962
+ }
3963
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3964
+ const state = {
3965
+ packageName,
3966
+ status: "running",
3967
+ currentStage: 0,
3968
+ startedAt: now,
3969
+ updatedAt: now,
3970
+ stages: config.stages.map((s, i) => ({
3971
+ ...s,
3972
+ scheduledAt: i === 0 ? now : void 0
3973
+ })),
3974
+ gates: config.gates
3975
+ };
3976
+ await writeTrainState(packageName, state);
3977
+ await executeStage(apiClient, packageName, state, 0);
3978
+ return state;
3979
+ }
3980
+ async function getTrainStatus(packageName) {
3981
+ return readTrainState(packageName);
3982
+ }
3983
+ async function pauseTrain(packageName) {
3984
+ const state = await readTrainState(packageName);
3985
+ if (!state || state.status !== "running") return state;
3986
+ state.status = "paused";
3987
+ state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3988
+ await writeTrainState(packageName, state);
3989
+ return state;
3990
+ }
3991
+ async function abortTrain(packageName) {
3992
+ await clearTrainState(packageName);
3993
+ }
3994
+ async function advanceTrain(apiClient, reportingClient, packageName) {
3995
+ const state = await readTrainState(packageName);
3996
+ if (!state || state.status !== "running") return state;
3997
+ const nextStage = state.currentStage + 1;
3998
+ if (nextStage >= state.stages.length) {
3999
+ state.status = "completed";
4000
+ state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
4001
+ await writeTrainState(packageName, state);
4002
+ return state;
4003
+ }
4004
+ const nextStageConfig = state.stages[nextStage];
4005
+ if (!nextStageConfig) return state;
4006
+ if (nextStageConfig.after) {
4007
+ const delayMs = parseDuration2(nextStageConfig.after);
4008
+ const currentStageConfig = state.stages[state.currentStage];
4009
+ const executedAt = currentStageConfig?.executedAt;
4010
+ if (executedAt && delayMs > 0) {
4011
+ const elapsed = Date.now() - new Date(executedAt).getTime();
4012
+ if (elapsed < delayMs) {
4013
+ const readyAt = new Date(new Date(executedAt).getTime() + delayMs).toISOString();
4014
+ nextStageConfig.scheduledAt = readyAt;
4015
+ await writeTrainState(packageName, state);
4016
+ return state;
4017
+ }
4018
+ }
4019
+ }
4020
+ if (state.gates) {
4021
+ if (state.gates.crashes?.max !== void 0) {
4022
+ const result = await getVitalsCrashes(reportingClient, packageName, { days: 1 });
4023
+ const latestRow = result.rows?.[result.rows.length - 1];
4024
+ const firstMetric = latestRow?.metrics ? Object.keys(latestRow.metrics)[0] : void 0;
4025
+ const value = firstMetric ? Number(latestRow?.metrics[firstMetric]?.decimalValue?.value) : void 0;
4026
+ if (value !== void 0 && value > state.gates.crashes.max / 100) {
4027
+ state.status = "paused";
4028
+ state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
4029
+ await writeTrainState(packageName, state);
4030
+ throw new Error(
4031
+ `Crash gate failed: ${(value * 100).toFixed(3)}% > max ${state.gates.crashes.max}%. Train paused.`
4032
+ );
4033
+ }
4034
+ }
4035
+ if (state.gates.anr?.max !== void 0) {
4036
+ const result = await getVitalsAnr(reportingClient, packageName, { days: 1 });
4037
+ const latestRow = result.rows?.[result.rows.length - 1];
4038
+ const firstMetric = latestRow?.metrics ? Object.keys(latestRow.metrics)[0] : void 0;
4039
+ const value = firstMetric ? Number(latestRow?.metrics[firstMetric]?.decimalValue?.value) : void 0;
4040
+ if (value !== void 0 && value > state.gates.anr.max / 100) {
4041
+ state.status = "paused";
4042
+ state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
4043
+ await writeTrainState(packageName, state);
4044
+ throw new Error(
4045
+ `ANR gate failed: ${(value * 100).toFixed(3)}% > max ${state.gates.anr.max}%. Train paused.`
4046
+ );
4047
+ }
4048
+ }
4049
+ }
4050
+ await executeStage(apiClient, packageName, state, nextStage);
4051
+ return state;
4052
+ }
4053
+ async function executeStage(apiClient, packageName, state, stageIndex) {
4054
+ const stage = state.stages[stageIndex];
4055
+ if (!stage) throw new Error(`Stage ${stageIndex} not found`);
4056
+ const rolloutFraction = stage.rollout / 100;
4057
+ await updateRollout(apiClient, packageName, stage.track, "increase", rolloutFraction);
4058
+ stage.executedAt = (/* @__PURE__ */ new Date()).toISOString();
4059
+ state.currentStage = stageIndex;
4060
+ state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
4061
+ await writeTrainState(packageName, state);
4062
+ }
4063
+
4064
+ // src/commands/games.ts
4065
+ async function listLeaderboards(client, packageName) {
4066
+ const result = await client.leaderboards.list(packageName);
4067
+ return result.items ?? [];
4068
+ }
4069
+ async function listAchievements(client, packageName) {
4070
+ const result = await client.achievements.list(packageName);
4071
+ return result.items ?? [];
4072
+ }
4073
+ async function listEvents(client, packageName) {
4074
+ const result = await client.events.list(packageName);
4075
+ return result.items ?? [];
4076
+ }
4077
+
4078
+ // src/commands/enterprise.ts
4079
+ async function listEnterpriseApps(client, organizationId) {
4080
+ const result = await client.apps.list(organizationId);
4081
+ return result.customApps ?? [];
4082
+ }
4083
+ async function createEnterpriseApp(client, organizationId, app) {
4084
+ return client.apps.create(organizationId, app);
4085
+ }
4086
+
4087
+ // src/audit.ts
4088
+ import { appendFile, chmod, mkdir as mkdir4, readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
4089
+ import { join as join6 } from "path";
4090
+ var auditDir = null;
4091
+ function initAudit(configDir) {
4092
+ auditDir = configDir;
4093
+ }
4094
+ async function writeAuditLog(entry) {
4095
+ if (!auditDir) return;
4096
+ try {
4097
+ await mkdir4(auditDir, { recursive: true, mode: 448 });
4098
+ const logPath = join6(auditDir, "audit.log");
4099
+ const redactedEntry = redactAuditArgs(entry);
4100
+ const line = JSON.stringify(redactedEntry) + "\n";
4101
+ await appendFile(logPath, line, { encoding: "utf-8", mode: 384 });
4102
+ await chmod(logPath, 384).catch(() => {
4103
+ });
4104
+ } catch {
4105
+ }
4106
+ }
4107
+ var SENSITIVE_ARG_KEYS = /* @__PURE__ */ new Set([
4108
+ "keyFile",
4109
+ "key_file",
4110
+ "serviceAccount",
4111
+ "service-account",
4112
+ "token",
4113
+ "password",
4114
+ "secret",
4115
+ "credentials",
4116
+ "private_key",
4117
+ "privateKey",
4118
+ "private_key_id",
4119
+ "privateKeyId",
4120
+ "client_secret",
4121
+ "clientSecret",
4122
+ "accessToken",
4123
+ "access_token",
4124
+ "refreshToken",
4125
+ "refresh_token",
4126
+ "apiKey",
4127
+ "api_key",
4128
+ "auth_token",
4129
+ "bearer",
4130
+ "jwt",
4131
+ "signing_key",
4132
+ "keystore_password",
4133
+ "store_password",
4134
+ "key_password"
4135
+ ]);
4136
+ function redactAuditArgs(entry) {
4137
+ const redacted = {};
4138
+ for (const [k, v] of Object.entries(entry.args)) {
4139
+ redacted[k] = SENSITIVE_ARG_KEYS.has(k) ? "[REDACTED]" : v;
4140
+ }
4141
+ return { ...entry, args: redacted };
4142
+ }
4143
+ function createAuditEntry(command, args, app) {
4144
+ return {
4145
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4146
+ command,
4147
+ app,
4148
+ args
4149
+ };
4150
+ }
4151
+ async function listAuditEvents(options) {
4152
+ if (!auditDir) return [];
4153
+ const logPath = join6(auditDir, "audit.log");
4154
+ let content;
4155
+ try {
4156
+ content = await readFile9(logPath, "utf-8");
4157
+ } catch {
4158
+ return [];
4159
+ }
4160
+ const lines = content.trim().split("\n").filter(Boolean);
4161
+ let entries = [];
4162
+ for (const line of lines) {
4163
+ try {
4164
+ entries.push(JSON.parse(line));
4165
+ } catch {
4166
+ }
4167
+ }
4168
+ if (options?.since) {
4169
+ const sinceDate = new Date(options.since).getTime();
4170
+ entries = entries.filter((e) => new Date(e.timestamp).getTime() >= sinceDate);
4171
+ }
4172
+ if (options?.command) {
4173
+ const cmd = options.command.toLowerCase();
4174
+ entries = entries.filter((e) => e.command.toLowerCase().includes(cmd));
4175
+ }
4176
+ if (options?.limit) {
4177
+ entries = entries.slice(-options.limit);
4178
+ }
4179
+ return entries;
4180
+ }
4181
+ async function searchAuditEvents(query) {
4182
+ const all = await listAuditEvents();
4183
+ const q = query.toLowerCase();
4184
+ return all.filter((e) => {
4185
+ const text = JSON.stringify(e).toLowerCase();
4186
+ return text.includes(q);
4187
+ });
4188
+ }
4189
+ async function clearAuditLog(options) {
4190
+ if (!auditDir) return { deleted: 0, remaining: 0 };
4191
+ const logPath = join6(auditDir, "audit.log");
4192
+ let content;
4193
+ try {
4194
+ content = await readFile9(logPath, "utf-8");
4195
+ } catch {
4196
+ return { deleted: 0, remaining: 0 };
4197
+ }
4198
+ const lines = content.trim().split("\n").filter(Boolean);
4199
+ if (!options?.before) {
4200
+ const count = lines.length;
4201
+ if (!options?.dryRun) {
4202
+ await writeFile5(logPath, "", { encoding: "utf-8", mode: 384 });
4203
+ }
4204
+ return { deleted: count, remaining: 0 };
4205
+ }
4206
+ const beforeDate = new Date(options.before).getTime();
4207
+ const keep = [];
4208
+ const remove = [];
4209
+ for (const line of lines) {
4210
+ try {
4211
+ const entry = JSON.parse(line);
4212
+ if (new Date(entry.timestamp).getTime() < beforeDate) {
4213
+ remove.push(line);
4214
+ } else {
4215
+ keep.push(line);
4216
+ }
4217
+ } catch {
4218
+ keep.push(line);
4219
+ }
4220
+ }
4221
+ if (!options?.dryRun) {
4222
+ await writeFile5(logPath, keep.length > 0 ? keep.join("\n") + "\n" : "", {
4223
+ encoding: "utf-8",
4224
+ mode: 384
4225
+ });
3194
4226
  }
4227
+ return { deleted: remove.length, remaining: keep.length };
3195
4228
  }
3196
4229
 
3197
- // src/utils/spinner.ts
3198
- import process3 from "process";
3199
- var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
3200
- var INTERVAL_MS = 80;
3201
- function createSpinner(message) {
3202
- const isTTY = process3.stderr.isTTY === true;
3203
- let frameIndex = 0;
3204
- let timer;
3205
- let currentMessage = message;
3206
- let started = false;
3207
- function clearLine() {
3208
- if (isTTY) {
3209
- process3.stderr.write("\r\x1B[K");
3210
- }
3211
- }
3212
- function renderFrame() {
3213
- const frame = FRAMES[frameIndex % FRAMES.length];
3214
- process3.stderr.write(`\r\x1B[K${frame} ${currentMessage}`);
3215
- frameIndex++;
4230
+ // src/commands/quota.ts
4231
+ var DAILY_LIMIT = 2e5;
4232
+ var MINUTE_LIMIT = 3e3;
4233
+ async function getQuotaUsage() {
4234
+ const now = Date.now();
4235
+ const startOfDay = new Date(now);
4236
+ startOfDay.setUTCHours(0, 0, 0, 0);
4237
+ const startOfMinute = new Date(now - 60 * 1e3);
4238
+ const todayEntries = await listAuditEvents({
4239
+ since: startOfDay.toISOString()
4240
+ });
4241
+ const minuteEntries = todayEntries.filter(
4242
+ (e) => new Date(e.timestamp).getTime() >= startOfMinute.getTime()
4243
+ );
4244
+ const commandCounts = /* @__PURE__ */ new Map();
4245
+ for (const entry of todayEntries) {
4246
+ commandCounts.set(entry.command, (commandCounts.get(entry.command) ?? 0) + 1);
3216
4247
  }
4248
+ const topCommands = Array.from(commandCounts.entries()).map(([command, count]) => ({ command, count })).sort((a, b) => b.count - a.count).slice(0, 10);
4249
+ const dailyCallsUsed = todayEntries.length;
4250
+ const minuteCallsUsed = minuteEntries.length;
3217
4251
  return {
3218
- start() {
3219
- if (started) return;
3220
- started = true;
3221
- if (!isTTY) {
3222
- process3.stderr.write(`${currentMessage}
3223
- `);
3224
- return;
3225
- }
3226
- renderFrame();
3227
- timer = setInterval(renderFrame, INTERVAL_MS);
3228
- },
3229
- stop(msg) {
3230
- if (timer) {
3231
- clearInterval(timer);
3232
- timer = void 0;
3233
- }
3234
- const text = msg ?? currentMessage;
3235
- if (isTTY) {
3236
- clearLine();
3237
- process3.stderr.write(`\u2714 ${text}
3238
- `);
3239
- } else if (!started) {
3240
- process3.stderr.write(`${text}
3241
- `);
3242
- }
3243
- started = false;
3244
- },
3245
- fail(msg) {
3246
- if (timer) {
3247
- clearInterval(timer);
3248
- timer = void 0;
3249
- }
3250
- const text = msg ?? currentMessage;
3251
- if (isTTY) {
3252
- clearLine();
3253
- process3.stderr.write(`\u2718 ${text}
3254
- `);
3255
- } else if (!started) {
3256
- process3.stderr.write(`${text}
3257
- `);
3258
- }
3259
- started = false;
3260
- },
3261
- update(msg) {
3262
- currentMessage = msg;
3263
- if (!isTTY || !started) return;
3264
- renderFrame();
3265
- }
4252
+ dailyCallsUsed,
4253
+ dailyCallsLimit: DAILY_LIMIT,
4254
+ dailyCallsRemaining: Math.max(0, DAILY_LIMIT - dailyCallsUsed),
4255
+ minuteCallsUsed,
4256
+ minuteCallsLimit: MINUTE_LIMIT,
4257
+ minuteCallsRemaining: Math.max(0, MINUTE_LIMIT - minuteCallsUsed),
4258
+ topCommands
3266
4259
  };
3267
4260
  }
3268
4261
 
@@ -3326,16 +4319,16 @@ function sortResults(items, sortSpec) {
3326
4319
  }
3327
4320
 
3328
4321
  // src/commands/plugin-scaffold.ts
3329
- import { mkdir as mkdir3, writeFile as writeFile4 } from "fs/promises";
3330
- import { join as join5 } from "path";
4322
+ import { mkdir as mkdir5, writeFile as writeFile6 } from "fs/promises";
4323
+ import { join as join7 } from "path";
3331
4324
  async function scaffoldPlugin(options) {
3332
4325
  const { name, dir, description = `GPC plugin: ${name}` } = options;
3333
4326
  const pluginName = name.startsWith("gpc-plugin-") ? name : `gpc-plugin-${name}`;
3334
4327
  const shortName = pluginName.replace(/^gpc-plugin-/, "");
3335
- const srcDir = join5(dir, "src");
3336
- const testDir = join5(dir, "tests");
3337
- await mkdir3(srcDir, { recursive: true });
3338
- await mkdir3(testDir, { recursive: true });
4328
+ const srcDir = join7(dir, "src");
4329
+ const testDir = join7(dir, "tests");
4330
+ await mkdir5(srcDir, { recursive: true });
4331
+ await mkdir5(testDir, { recursive: true });
3339
4332
  const files = [];
3340
4333
  const pkg = {
3341
4334
  name: pluginName,
@@ -3369,7 +4362,7 @@ async function scaffoldPlugin(options) {
3369
4362
  vitest: "^3.0.0"
3370
4363
  }
3371
4364
  };
3372
- await writeFile4(join5(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
4365
+ await writeFile6(join7(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
3373
4366
  files.push("package.json");
3374
4367
  const tsconfig = {
3375
4368
  compilerOptions: {
@@ -3385,7 +4378,7 @@ async function scaffoldPlugin(options) {
3385
4378
  },
3386
4379
  include: ["src"]
3387
4380
  };
3388
- await writeFile4(join5(dir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2) + "\n");
4381
+ await writeFile6(join7(dir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2) + "\n");
3389
4382
  files.push("tsconfig.json");
3390
4383
  const srcContent = `import { definePlugin } from "@gpc-cli/plugin-sdk";
3391
4384
  import type { CommandEvent, CommandResult } from "@gpc-cli/plugin-sdk";
@@ -3418,7 +4411,7 @@ export const plugin = definePlugin({
3418
4411
  },
3419
4412
  });
3420
4413
  `;
3421
- await writeFile4(join5(srcDir, "index.ts"), srcContent);
4414
+ await writeFile6(join7(srcDir, "index.ts"), srcContent);
3422
4415
  files.push("src/index.ts");
3423
4416
  const testContent = `import { describe, it, expect, vi } from "vitest";
3424
4417
  import { plugin } from "../src/index";
@@ -3443,154 +4436,11 @@ describe("${pluginName}", () => {
3443
4436
  });
3444
4437
  });
3445
4438
  `;
3446
- await writeFile4(join5(testDir, "plugin.test.ts"), testContent);
4439
+ await writeFile6(join7(testDir, "plugin.test.ts"), testContent);
3447
4440
  files.push("tests/plugin.test.ts");
3448
4441
  return { dir, files };
3449
4442
  }
3450
4443
 
3451
- // src/audit.ts
3452
- import { appendFile, chmod, mkdir as mkdir4, readFile as readFile8, writeFile as writeFile5 } from "fs/promises";
3453
- import { join as join6 } from "path";
3454
- var auditDir = null;
3455
- function initAudit(configDir) {
3456
- auditDir = configDir;
3457
- }
3458
- async function writeAuditLog(entry) {
3459
- if (!auditDir) return;
3460
- try {
3461
- await mkdir4(auditDir, { recursive: true, mode: 448 });
3462
- const logPath = join6(auditDir, "audit.log");
3463
- const redactedEntry = redactAuditArgs(entry);
3464
- const line = JSON.stringify(redactedEntry) + "\n";
3465
- await appendFile(logPath, line, { encoding: "utf-8", mode: 384 });
3466
- await chmod(logPath, 384).catch(() => {
3467
- });
3468
- } catch {
3469
- }
3470
- }
3471
- var SENSITIVE_ARG_KEYS = /* @__PURE__ */ new Set([
3472
- "keyFile",
3473
- "key_file",
3474
- "serviceAccount",
3475
- "service-account",
3476
- "token",
3477
- "password",
3478
- "secret",
3479
- "credentials",
3480
- "private_key",
3481
- "privateKey",
3482
- "private_key_id",
3483
- "privateKeyId",
3484
- "client_secret",
3485
- "clientSecret",
3486
- "accessToken",
3487
- "access_token",
3488
- "refreshToken",
3489
- "refresh_token",
3490
- "apiKey",
3491
- "api_key",
3492
- "auth_token",
3493
- "bearer",
3494
- "jwt",
3495
- "signing_key",
3496
- "keystore_password",
3497
- "store_password",
3498
- "key_password"
3499
- ]);
3500
- function redactAuditArgs(entry) {
3501
- const redacted = {};
3502
- for (const [k, v] of Object.entries(entry.args)) {
3503
- redacted[k] = SENSITIVE_ARG_KEYS.has(k) ? "[REDACTED]" : v;
3504
- }
3505
- return { ...entry, args: redacted };
3506
- }
3507
- function createAuditEntry(command, args, app) {
3508
- return {
3509
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3510
- command,
3511
- app,
3512
- args
3513
- };
3514
- }
3515
- async function listAuditEvents(options) {
3516
- if (!auditDir) return [];
3517
- const logPath = join6(auditDir, "audit.log");
3518
- let content;
3519
- try {
3520
- content = await readFile8(logPath, "utf-8");
3521
- } catch {
3522
- return [];
3523
- }
3524
- const lines = content.trim().split("\n").filter(Boolean);
3525
- let entries = [];
3526
- for (const line of lines) {
3527
- try {
3528
- entries.push(JSON.parse(line));
3529
- } catch {
3530
- }
3531
- }
3532
- if (options?.since) {
3533
- const sinceDate = new Date(options.since).getTime();
3534
- entries = entries.filter((e) => new Date(e.timestamp).getTime() >= sinceDate);
3535
- }
3536
- if (options?.command) {
3537
- const cmd = options.command.toLowerCase();
3538
- entries = entries.filter((e) => e.command.toLowerCase().includes(cmd));
3539
- }
3540
- if (options?.limit) {
3541
- entries = entries.slice(-options.limit);
3542
- }
3543
- return entries;
3544
- }
3545
- async function searchAuditEvents(query) {
3546
- const all = await listAuditEvents();
3547
- const q = query.toLowerCase();
3548
- return all.filter((e) => {
3549
- const text = JSON.stringify(e).toLowerCase();
3550
- return text.includes(q);
3551
- });
3552
- }
3553
- async function clearAuditLog(options) {
3554
- if (!auditDir) return { deleted: 0, remaining: 0 };
3555
- const logPath = join6(auditDir, "audit.log");
3556
- let content;
3557
- try {
3558
- content = await readFile8(logPath, "utf-8");
3559
- } catch {
3560
- return { deleted: 0, remaining: 0 };
3561
- }
3562
- const lines = content.trim().split("\n").filter(Boolean);
3563
- if (!options?.before) {
3564
- const count = lines.length;
3565
- if (!options?.dryRun) {
3566
- await writeFile5(logPath, "", { encoding: "utf-8", mode: 384 });
3567
- }
3568
- return { deleted: count, remaining: 0 };
3569
- }
3570
- const beforeDate = new Date(options.before).getTime();
3571
- const keep = [];
3572
- const remove = [];
3573
- for (const line of lines) {
3574
- try {
3575
- const entry = JSON.parse(line);
3576
- if (new Date(entry.timestamp).getTime() < beforeDate) {
3577
- remove.push(line);
3578
- } else {
3579
- keep.push(line);
3580
- }
3581
- } catch {
3582
- keep.push(line);
3583
- }
3584
- }
3585
- if (!options?.dryRun) {
3586
- await writeFile5(logPath, keep.length > 0 ? keep.join("\n") + "\n" : "", {
3587
- encoding: "utf-8",
3588
- mode: 384
3589
- });
3590
- }
3591
- return { deleted: remove.length, remaining: keep.length };
3592
- }
3593
-
3594
4444
  // src/utils/webhooks.ts
3595
4445
  function formatSlackPayload(payload) {
3596
4446
  const status = payload.success ? "\u2713" : "\u2717";
@@ -3736,7 +4586,7 @@ function detectFileType(filePath) {
3736
4586
  }
3737
4587
 
3738
4588
  // src/commands/generated-apks.ts
3739
- import { writeFile as writeFile6 } from "fs/promises";
4589
+ import { writeFile as writeFile7 } from "fs/promises";
3740
4590
  async function listGeneratedApks(client, packageName, versionCode) {
3741
4591
  if (!Number.isInteger(versionCode) || versionCode <= 0) {
3742
4592
  throw new GpcError(
@@ -3767,7 +4617,7 @@ async function downloadGeneratedApk(client, packageName, versionCode, apkId, out
3767
4617
  }
3768
4618
  const buffer = await client.generatedApks.download(packageName, versionCode, apkId);
3769
4619
  const bytes = new Uint8Array(buffer);
3770
- await writeFile6(outputPath, bytes);
4620
+ await writeFile7(outputPath, bytes);
3771
4621
  return { path: outputPath, sizeBytes: bytes.byteLength };
3772
4622
  }
3773
4623
 
@@ -3834,17 +4684,19 @@ async function deactivatePurchaseOption(client, packageName, purchaseOptionId) {
3834
4684
  }
3835
4685
 
3836
4686
  // src/commands/bundle-analysis.ts
3837
- import { readFile as readFile9, stat as stat7 } from "fs/promises";
4687
+ import { readFile as readFile10, stat as stat7 } from "fs/promises";
3838
4688
  var EOCD_SIGNATURE = 101010256;
3839
4689
  var CD_SIGNATURE = 33639248;
3840
4690
  var MODULE_SUBDIRS = /* @__PURE__ */ new Set(["dex", "manifest", "res", "assets", "lib", "resources.pb", "root"]);
3841
4691
  function detectCategory(path) {
3842
4692
  const lower = path.toLowerCase();
3843
4693
  if (lower.endsWith(".dex") || /\/dex\/[^/]+\.dex$/.test(lower)) return "dex";
3844
- if (lower === "resources.arsc" || lower.endsWith("/resources.arsc") || lower.endsWith("/resources.pb") || /^(([^/]+\/)?res\/)/.test(lower)) return "resources";
4694
+ if (lower === "resources.arsc" || lower.endsWith("/resources.arsc") || lower.endsWith("/resources.pb") || /^(([^/]+\/)?res\/)/.test(lower))
4695
+ return "resources";
3845
4696
  if (/^(([^/]+\/)?assets\/)/.test(lower)) return "assets";
3846
4697
  if (/^(([^/]+\/)?lib\/)/.test(lower)) return "native-libs";
3847
- if (lower === "androidmanifest.xml" || lower.endsWith("/androidmanifest.xml") || /^(([^/]+\/)?manifest\/)/.test(lower)) return "manifest";
4698
+ if (lower === "androidmanifest.xml" || lower.endsWith("/androidmanifest.xml") || /^(([^/]+\/)?manifest\/)/.test(lower))
4699
+ return "manifest";
3848
4700
  if (lower.startsWith("meta-inf/") || lower === "meta-inf") return "signing";
3849
4701
  return "other";
3850
4702
  }
@@ -3912,7 +4764,7 @@ async function analyzeBundle(filePath) {
3912
4764
  if (!fileInfo || !fileInfo.isFile()) {
3913
4765
  throw new Error(`File not found: ${filePath}`);
3914
4766
  }
3915
- const buf = await readFile9(filePath);
4767
+ const buf = await readFile10(filePath);
3916
4768
  const cdEntries = parseCentralDirectory(buf);
3917
4769
  const fileType = detectFileType2(filePath);
3918
4770
  const isAab = fileType === "aab";
@@ -3925,7 +4777,11 @@ async function analyzeBundle(filePath) {
3925
4777
  }));
3926
4778
  const moduleMap = /* @__PURE__ */ new Map();
3927
4779
  for (const entry of entries) {
3928
- const existing = moduleMap.get(entry.module) ?? { compressedSize: 0, uncompressedSize: 0, entries: 0 };
4780
+ const existing = moduleMap.get(entry.module) ?? {
4781
+ compressedSize: 0,
4782
+ uncompressedSize: 0,
4783
+ entries: 0
4784
+ };
3929
4785
  existing.compressedSize += entry.compressedSize;
3930
4786
  existing.uncompressedSize += entry.uncompressedSize;
3931
4787
  existing.entries += 1;
@@ -3933,7 +4789,11 @@ async function analyzeBundle(filePath) {
3933
4789
  }
3934
4790
  const categoryMap = /* @__PURE__ */ new Map();
3935
4791
  for (const entry of entries) {
3936
- const existing = categoryMap.get(entry.category) ?? { compressedSize: 0, uncompressedSize: 0, entries: 0 };
4792
+ const existing = categoryMap.get(entry.category) ?? {
4793
+ compressedSize: 0,
4794
+ uncompressedSize: 0,
4795
+ entries: 0
4796
+ };
3937
4797
  existing.compressedSize += entry.compressedSize;
3938
4798
  existing.uncompressedSize += entry.uncompressedSize;
3939
4799
  existing.entries += 1;
@@ -3984,19 +4844,52 @@ function compareBundles(before, after) {
3984
4844
  categoryDeltas
3985
4845
  };
3986
4846
  }
4847
+ function topFiles(analysis, n = 20) {
4848
+ return [...analysis.entries].sort((a, b) => b.compressedSize - a.compressedSize).slice(0, n);
4849
+ }
4850
+ async function checkBundleSize(analysis, configPath = ".bundlesize.json") {
4851
+ let config;
4852
+ try {
4853
+ const raw = await readFile10(configPath, "utf-8");
4854
+ config = JSON.parse(raw);
4855
+ } catch {
4856
+ return { passed: true, violations: [] };
4857
+ }
4858
+ const violations = [];
4859
+ if (config.maxTotalCompressed !== void 0 && analysis.totalCompressed > config.maxTotalCompressed) {
4860
+ violations.push({
4861
+ subject: "total",
4862
+ actual: analysis.totalCompressed,
4863
+ max: config.maxTotalCompressed
4864
+ });
4865
+ }
4866
+ if (config.modules) {
4867
+ for (const [moduleName, threshold] of Object.entries(config.modules)) {
4868
+ const mod = analysis.modules.find((m) => m.name === moduleName);
4869
+ if (mod && mod.compressedSize > threshold.maxCompressed) {
4870
+ violations.push({
4871
+ subject: `module:${moduleName}`,
4872
+ actual: mod.compressedSize,
4873
+ max: threshold.maxCompressed
4874
+ });
4875
+ }
4876
+ }
4877
+ }
4878
+ return { passed: violations.length === 0, violations };
4879
+ }
3987
4880
 
3988
4881
  // src/commands/status.ts
3989
- import { mkdir as mkdir5, readFile as readFile10, writeFile as writeFile7 } from "fs/promises";
3990
- import { execSync } from "child_process";
3991
- import { join as join7 } from "path";
3992
- import { getCacheDir } from "@gpc-cli/config";
4882
+ import { mkdir as mkdir6, readFile as readFile11, writeFile as writeFile8 } from "fs/promises";
4883
+ import { execFile as execFile2 } from "child_process";
4884
+ import { join as join8 } from "path";
4885
+ import { getCacheDir as getCacheDir2 } from "@gpc-cli/config";
3993
4886
  var DEFAULT_TTL_SECONDS = 3600;
3994
4887
  function cacheFilePath(packageName) {
3995
- return join7(getCacheDir(), `status-${packageName}.json`);
4888
+ return join8(getCacheDir2(), `status-${packageName}.json`);
3996
4889
  }
3997
4890
  async function loadStatusCache(packageName, ttlSeconds = DEFAULT_TTL_SECONDS) {
3998
4891
  try {
3999
- const raw = await readFile10(cacheFilePath(packageName), "utf-8");
4892
+ const raw = await readFile11(cacheFilePath(packageName), "utf-8");
4000
4893
  const entry = JSON.parse(raw);
4001
4894
  const age = (Date.now() - new Date(entry.fetchedAt).getTime()) / 1e3;
4002
4895
  if (age > (entry.ttl ?? ttlSeconds)) return null;
@@ -4012,10 +4905,10 @@ async function loadStatusCache(packageName, ttlSeconds = DEFAULT_TTL_SECONDS) {
4012
4905
  }
4013
4906
  async function saveStatusCache(packageName, data, ttlSeconds = DEFAULT_TTL_SECONDS) {
4014
4907
  try {
4015
- const dir = getCacheDir();
4016
- await mkdir5(dir, { recursive: true });
4908
+ const dir = getCacheDir2();
4909
+ await mkdir6(dir, { recursive: true });
4017
4910
  const entry = { fetchedAt: data.fetchedAt, ttl: ttlSeconds, data };
4018
- await writeFile7(cacheFilePath(packageName), JSON.stringify(entry, null, 2), {
4911
+ await writeFile8(cacheFilePath(packageName), JSON.stringify(entry, null, 2), {
4019
4912
  encoding: "utf-8",
4020
4913
  mode: 384
4021
4914
  });
@@ -4126,7 +5019,14 @@ async function getAppStatus(client, reporting, packageName, options = {}) {
4126
5019
  slowStartRate: options.vitalThresholds?.slowStartRate ?? DEFAULT_THRESHOLDS.slowStartRate,
4127
5020
  slowRenderingRate: options.vitalThresholds?.slowRenderingRate ?? DEFAULT_THRESHOLDS.slowRenderingRate
4128
5021
  };
4129
- const [releasesResult, crashesResult, anrResult, slowStartResult, slowRenderResult, reviewsResult] = await Promise.allSettled([
5022
+ const [
5023
+ releasesResult,
5024
+ crashesResult,
5025
+ anrResult,
5026
+ slowStartResult,
5027
+ slowRenderResult,
5028
+ reviewsResult
5029
+ ] = await Promise.allSettled([
4130
5030
  sections.has("releases") ? getReleasesStatus(client, packageName) : Promise.resolve([]),
4131
5031
  sections.has("vitals") ? queryVitalWithTrend(reporting, packageName, "crashRateMetricSet", days) : Promise.resolve(SKIPPED_VITAL),
4132
5032
  sections.has("vitals") ? queryVitalWithTrend(reporting, packageName, "anrRateMetricSet", days) : Promise.resolve(SKIPPED_VITAL),
@@ -4155,7 +5055,12 @@ async function getAppStatus(client, reporting, packageName, options = {}) {
4155
5055
  releases,
4156
5056
  vitals: {
4157
5057
  windowDays: days,
4158
- crashes: toVitalMetric(crashes.current, thresholds.crashRate, crashes.previous, crashes.trend),
5058
+ crashes: toVitalMetric(
5059
+ crashes.current,
5060
+ thresholds.crashRate,
5061
+ crashes.previous,
5062
+ crashes.trend
5063
+ ),
4159
5064
  anr: toVitalMetric(anr.current, thresholds.anrRate, anr.previous, anr.trend),
4160
5065
  slowStarts: toVitalMetric(
4161
5066
  slowStart.current,
@@ -4380,20 +5285,20 @@ async function runWatchLoop(opts) {
4380
5285
  }
4381
5286
  }
4382
5287
  function breachStateFilePath(packageName) {
4383
- return join7(getCacheDir(), `breach-state-${packageName}.json`);
5288
+ return join8(getCacheDir2(), `breach-state-${packageName}.json`);
4384
5289
  }
4385
5290
  async function trackBreachState(packageName, isBreaching) {
4386
5291
  const filePath = breachStateFilePath(packageName);
4387
5292
  let prevBreaching = false;
4388
5293
  try {
4389
- const raw = await readFile10(filePath, "utf-8");
5294
+ const raw = await readFile11(filePath, "utf-8");
4390
5295
  prevBreaching = JSON.parse(raw).breaching;
4391
5296
  } catch {
4392
5297
  }
4393
5298
  if (prevBreaching !== isBreaching) {
4394
5299
  try {
4395
- await mkdir5(getCacheDir(), { recursive: true });
4396
- await writeFile7(
5300
+ await mkdir6(getCacheDir2(), { recursive: true });
5301
+ await writeFile8(
4397
5302
  filePath,
4398
5303
  JSON.stringify({ breaching: isBreaching, since: (/* @__PURE__ */ new Date()).toISOString() }, null, 2),
4399
5304
  { encoding: "utf-8", mode: 384 }
@@ -4409,20 +5314,20 @@ function sendNotification(title, body) {
4409
5314
  try {
4410
5315
  const p = process.platform;
4411
5316
  if (p === "darwin") {
4412
- execSync(
4413
- `osascript -e 'display notification ${JSON.stringify(body)} with title ${JSON.stringify(title)}'`,
4414
- { stdio: "ignore" }
4415
- );
5317
+ execFile2("osascript", [
5318
+ "-e",
5319
+ `display notification ${JSON.stringify(body)} with title ${JSON.stringify(title)}`
5320
+ ]);
4416
5321
  } else if (p === "linux") {
4417
- execSync(`notify-send ${JSON.stringify(title)} ${JSON.stringify(body)}`, {
4418
- stdio: "ignore"
4419
- });
5322
+ execFile2("notify-send", [title, body]);
4420
5323
  } else if (p === "win32") {
4421
- const escaped = (s) => s.replace(/'/g, "''");
4422
- execSync(
4423
- `powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('${escaped(body)}', '${escaped(title)}')"`,
4424
- { stdio: "ignore" }
4425
- );
5324
+ const psEscape = (s) => s.replace(/'/g, "''").replace(/[\r\n]/g, " ");
5325
+ execFile2("powershell", [
5326
+ "-NoProfile",
5327
+ "-NonInteractive",
5328
+ "-Command",
5329
+ `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('${psEscape(body)}', '${psEscape(title)}')`
5330
+ ]);
4426
5331
  }
4427
5332
  } catch {
4428
5333
  }
@@ -4433,6 +5338,7 @@ function statusHasBreach(status) {
4433
5338
  export {
4434
5339
  ApiError,
4435
5340
  ConfigError,
5341
+ DEFAULT_LIMITS,
4436
5342
  GOOGLE_PLAY_LANGUAGES,
4437
5343
  GpcError,
4438
5344
  NetworkError,
@@ -4440,26 +5346,34 @@ export {
4440
5346
  PluginManager,
4441
5347
  SENSITIVE_ARG_KEYS,
4442
5348
  SENSITIVE_KEYS,
5349
+ abortTrain,
4443
5350
  acknowledgeProductPurchase,
4444
5351
  activateBasePlan,
4445
5352
  activateOffer,
4446
5353
  activatePurchaseOption,
4447
5354
  addRecoveryTargeting,
4448
5355
  addTesters,
5356
+ advanceTrain,
4449
5357
  analyzeBundle,
5358
+ analyzeRemoteListings,
5359
+ analyzeReviews2 as analyzeReviews,
4450
5360
  batchSyncInAppProducts,
4451
5361
  cancelRecoveryAction,
4452
5362
  cancelSubscriptionPurchase,
5363
+ checkBundleSize,
4453
5364
  checkThreshold,
4454
5365
  clearAuditLog,
4455
5366
  compareBundles,
5367
+ compareVersionVitals,
4456
5368
  compareVitalsTrend,
4457
5369
  computeStatusDiff,
4458
5370
  consumeProductPurchase,
4459
5371
  convertRegionPrices,
4460
5372
  createAuditEntry,
4461
5373
  createDeviceTier,
5374
+ createEnterpriseApp,
4462
5375
  createExternalTransaction,
5376
+ createGrant,
4463
5377
  createInAppProduct,
4464
5378
  createOffer,
4465
5379
  createOneTimeOffer,
@@ -4474,6 +5388,7 @@ export {
4474
5388
  deactivatePurchaseOption,
4475
5389
  deferSubscriptionPurchase,
4476
5390
  deleteBasePlan,
5391
+ deleteGrant,
4477
5392
  deleteImage,
4478
5393
  deleteInAppProduct,
4479
5394
  deleteListing,
@@ -4486,6 +5401,7 @@ export {
4486
5401
  detectOutputFormat,
4487
5402
  diffListings,
4488
5403
  diffListingsCommand,
5404
+ diffListingsEnhanced,
4489
5405
  diffOneTimeProduct,
4490
5406
  diffReleases,
4491
5407
  diffSubscription,
@@ -4503,6 +5419,7 @@ export {
4503
5419
  formatStatusDiff,
4504
5420
  formatStatusSummary,
4505
5421
  formatStatusTable,
5422
+ formatWordDiff,
4506
5423
  generateMigrationPlan,
4507
5424
  generateNotesFromGit,
4508
5425
  getAppInfo,
@@ -4518,15 +5435,19 @@ export {
4518
5435
  getOneTimeProduct,
4519
5436
  getProductPurchase,
4520
5437
  getPurchaseOption,
5438
+ getQuotaUsage,
4521
5439
  getReleasesStatus,
4522
5440
  getReview,
4523
5441
  getSubscription,
5442
+ getSubscriptionAnalytics,
4524
5443
  getSubscriptionPurchase,
5444
+ getTrainStatus,
4525
5445
  getUser,
4526
5446
  getVitalsAnomalies,
4527
5447
  getVitalsAnr,
4528
5448
  getVitalsBattery,
4529
5449
  getVitalsCrashes,
5450
+ getVitalsLmk,
4530
5451
  getVitalsMemory,
4531
5452
  getVitalsOverview,
4532
5453
  getVitalsRendering,
@@ -4540,11 +5461,19 @@ export {
4540
5461
  isValidBcp47,
4541
5462
  isValidReportType,
4542
5463
  isValidStatsDimension,
5464
+ lintListing,
5465
+ lintListings,
5466
+ lintLocalListings,
5467
+ listAchievements,
4543
5468
  listAuditEvents,
4544
5469
  listDeviceTiers,
5470
+ listEnterpriseApps,
5471
+ listEvents,
4545
5472
  listGeneratedApks,
5473
+ listGrants,
4546
5474
  listImages,
4547
5475
  listInAppProducts,
5476
+ listLeaderboards,
4548
5477
  listOffers,
4549
5478
  listOneTimeOffers,
4550
5479
  listOneTimeProducts,
@@ -4558,11 +5487,13 @@ export {
4558
5487
  listUsers,
4559
5488
  listVoidedPurchases,
4560
5489
  loadStatusCache,
5490
+ maybePaginate,
4561
5491
  migratePrices,
4562
5492
  parseAppfile,
4563
5493
  parseFastfile,
4564
5494
  parseGrantArg,
4565
5495
  parseMonth,
5496
+ pauseTrain,
4566
5497
  promoteRelease,
4567
5498
  publish,
4568
5499
  pullListings,
@@ -4573,6 +5504,7 @@ export {
4573
5504
  redactSensitive,
4574
5505
  refundExternalTransaction,
4575
5506
  refundOrder,
5507
+ refundSubscriptionV2,
4576
5508
  removeTesters,
4577
5509
  removeUser,
4578
5510
  replyToReview,
@@ -4587,11 +5519,14 @@ export {
4587
5519
  sendNotification,
4588
5520
  sendWebhook,
4589
5521
  sortResults,
5522
+ startTrain,
4590
5523
  statusHasBreach,
4591
5524
  syncInAppProducts,
5525
+ topFiles,
4592
5526
  trackBreachState,
4593
5527
  updateAppDetails,
4594
5528
  updateDataSafety,
5529
+ updateGrant,
4595
5530
  updateInAppProduct,
4596
5531
  updateListing,
4597
5532
  updateOffer,
@@ -4614,6 +5549,8 @@ export {
4614
5549
  validateTrackName,
4615
5550
  validateUploadFile,
4616
5551
  validateVersionCode,
5552
+ watchVitalsWithAutoHalt,
5553
+ wordDiff,
4617
5554
  writeAuditLog,
4618
5555
  writeListingsToDir,
4619
5556
  writeMigrationOutput