@gpc-cli/core 0.9.28 → 0.9.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/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,26 @@ 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(key.length, ...rows.map((row) => truncateCell(cellValue(row[key]), maxCellWidth).length))
175
202
  );
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");
203
+ const isNumeric = keys.map(
204
+ (key) => rows.every((row) => {
205
+ const v = cellValue(row[key]);
206
+ return v === "" || isNumericCell(v);
207
+ })
208
+ );
209
+ const header = keys.map((key, i) => bold(key.padEnd(widths[i] ?? 0))).join(" ");
210
+ const separator = widths.map((w) => dim("\u2500".repeat(w))).join(" ");
211
+ const body = rows.map(
212
+ (row) => keys.map((key, i) => {
213
+ const cell = truncateCell(cellValue(row[key]), maxCellWidth);
214
+ const w = widths[i] ?? 0;
215
+ return isNumeric[i] ? cell.padStart(w) : cell.padEnd(w);
216
+ }).join(" ")
217
+ ).join("\n");
179
218
  return `${header}
180
219
  ${separator}
181
220
  ${body}`;
@@ -284,6 +323,28 @@ function buildTestCase(item, commandName, index = 0) {
284
323
  failed: false
285
324
  };
286
325
  }
326
+ async function maybePaginate(output) {
327
+ const isTTY = process2.stdout.isTTY;
328
+ const termHeight = process2.stdout.rows ?? 24;
329
+ const lineCount = output.split("\n").length;
330
+ if (!isTTY || lineCount <= termHeight) {
331
+ console.log(output);
332
+ return;
333
+ }
334
+ const pager = process2.env["GPC_PAGER"] ?? process2.env["PAGER"] ?? "less";
335
+ try {
336
+ const { spawn } = await import("child_process");
337
+ const child = spawn(pager, [], {
338
+ stdio: ["pipe", "inherit", "inherit"],
339
+ env: { ...process2.env, LESS: process2.env["LESS"] ?? "-FRX" }
340
+ });
341
+ child.stdin.write(output);
342
+ child.stdin.end();
343
+ await new Promise((resolve2) => child.on("close", resolve2));
344
+ } catch {
345
+ console.log(output);
346
+ }
347
+ }
287
348
  function formatJunit(data, commandName = "command") {
288
349
  const { cases, failures } = toTestCases(data, commandName);
289
350
  const tests = cases.length;
@@ -496,6 +557,7 @@ async function getAppInfo(client, packageName) {
496
557
 
497
558
  // src/commands/releases.ts
498
559
  import { stat as stat2 } from "fs/promises";
560
+ import { ApiError as ApiError2 } from "@gpc-cli/api";
499
561
 
500
562
  // src/utils/file-validation.ts
501
563
  import { readFile, stat } from "fs/promises";
@@ -1161,6 +1223,70 @@ function diffListings(local, remote) {
1161
1223
  return diffs;
1162
1224
  }
1163
1225
 
1226
+ // src/utils/listing-text.ts
1227
+ var DEFAULT_LIMITS = {
1228
+ title: 30,
1229
+ shortDescription: 80,
1230
+ fullDescription: 4e3,
1231
+ video: 256
1232
+ };
1233
+ function lintListing(language, fields, limits = DEFAULT_LIMITS) {
1234
+ const fieldResults = [];
1235
+ for (const [field, limit] of Object.entries(limits)) {
1236
+ const value = fields[field] ?? "";
1237
+ const chars = [...value].length;
1238
+ const pct = Math.round(chars / limit * 100);
1239
+ let status = "ok";
1240
+ if (chars > limit) status = "over";
1241
+ else if (pct >= 80) status = "warn";
1242
+ fieldResults.push({ field, chars, limit, pct, status });
1243
+ }
1244
+ const valid = fieldResults.every((r) => r.status !== "over");
1245
+ return { language, fields: fieldResults, valid };
1246
+ }
1247
+ function lintListings(listings, limits) {
1248
+ return listings.map((l) => lintListing(l.language, l.fields, limits));
1249
+ }
1250
+ function tokenize(text) {
1251
+ return text.split(/(\s+)/);
1252
+ }
1253
+ function wordDiff(before, after) {
1254
+ const aTokens = tokenize(before);
1255
+ const bTokens = tokenize(after);
1256
+ const m = aTokens.length;
1257
+ const n = bTokens.length;
1258
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
1259
+ const cell = (r, c) => dp[r]?.[c] ?? 0;
1260
+ for (let i2 = 1; i2 <= m; i2++) {
1261
+ for (let j2 = 1; j2 <= n; j2++) {
1262
+ 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));
1263
+ }
1264
+ }
1265
+ const result = [];
1266
+ let i = m, j = n;
1267
+ while (i > 0 || j > 0) {
1268
+ if (i > 0 && j > 0 && aTokens[i - 1] === bTokens[j - 1]) {
1269
+ result.unshift({ text: aTokens[i - 1] ?? "", type: "equal" });
1270
+ i--;
1271
+ j--;
1272
+ } else if (j > 0 && (i === 0 || cell(i, j - 1) >= cell(i - 1, j))) {
1273
+ result.unshift({ text: bTokens[j - 1] ?? "", type: "insert" });
1274
+ j--;
1275
+ } else {
1276
+ result.unshift({ text: aTokens[i - 1] ?? "", type: "delete" });
1277
+ i--;
1278
+ }
1279
+ }
1280
+ return result;
1281
+ }
1282
+ function formatWordDiff(diff) {
1283
+ return diff.map((t) => {
1284
+ if (t.type === "equal") return t.text;
1285
+ if (t.type === "insert") return `[+${t.text}]`;
1286
+ return `[-${t.text}]`;
1287
+ }).join("");
1288
+ }
1289
+
1164
1290
  // src/commands/listings.ts
1165
1291
  function validateLanguage(lang) {
1166
1292
  if (!isValidBcp47(lang)) {
@@ -1231,6 +1357,57 @@ async function pullListings(client, packageName, dir) {
1231
1357
  throw error;
1232
1358
  }
1233
1359
  }
1360
+ async function lintLocalListings(dir) {
1361
+ const localListings = await readListingsFromDir(dir);
1362
+ return lintListings(
1363
+ localListings.map((l) => ({
1364
+ language: l.language,
1365
+ fields: {
1366
+ title: l.title,
1367
+ shortDescription: l.shortDescription,
1368
+ fullDescription: l.fullDescription,
1369
+ video: l.video
1370
+ }
1371
+ }))
1372
+ );
1373
+ }
1374
+ async function analyzeRemoteListings(client, packageName, options) {
1375
+ const listings = await getListings(client, packageName);
1376
+ const results = lintListings(
1377
+ listings.map((l) => ({
1378
+ language: l.language,
1379
+ fields: {
1380
+ title: l.title,
1381
+ shortDescription: l.shortDescription,
1382
+ fullDescription: l.fullDescription,
1383
+ video: l["video"]
1384
+ }
1385
+ }))
1386
+ );
1387
+ let missingLocales;
1388
+ if (options?.expectedLocales) {
1389
+ const present = new Set(listings.map((l) => l.language));
1390
+ missingLocales = options.expectedLocales.filter((loc) => !present.has(loc));
1391
+ }
1392
+ return { results, missingLocales };
1393
+ }
1394
+ async function diffListingsEnhanced(client, packageName, dir, options) {
1395
+ const allDiffs = await diffListingsCommand(client, packageName, dir);
1396
+ let result = allDiffs;
1397
+ if (options?.lang) {
1398
+ result = allDiffs.filter((d) => d.language === options.lang);
1399
+ }
1400
+ if (options?.wordLevel) {
1401
+ return result.map((d) => {
1402
+ if (d.field === "fullDescription" && d.local && d.remote) {
1403
+ const diff = wordDiff(d.remote, d.local);
1404
+ return { ...d, diffSummary: formatWordDiff(diff) };
1405
+ }
1406
+ return d;
1407
+ });
1408
+ }
1409
+ return result;
1410
+ }
1234
1411
  async function pushListings(client, packageName, dir, options) {
1235
1412
  const localListings = await readListingsFromDir(dir);
1236
1413
  if (localListings.length === 0) {
@@ -1244,6 +1421,33 @@ async function pushListings(client, packageName, dir, options) {
1244
1421
  for (const listing of localListings) {
1245
1422
  validateLanguage(listing.language);
1246
1423
  }
1424
+ if (!options?.force) {
1425
+ const lintResults = lintListings(
1426
+ localListings.map((l) => ({
1427
+ language: l.language,
1428
+ fields: {
1429
+ title: l.title,
1430
+ shortDescription: l.shortDescription,
1431
+ fullDescription: l.fullDescription,
1432
+ video: l["video"]
1433
+ }
1434
+ }))
1435
+ );
1436
+ const overLimit = lintResults.filter((r) => !r.valid);
1437
+ if (overLimit.length > 0) {
1438
+ const details = overLimit.map((r) => {
1439
+ const over = r.fields.filter((f) => f.status === "over");
1440
+ return `${r.language}: ${over.map((f) => `${f.field} (${f.chars}/${f.limit})`).join(", ")}`;
1441
+ }).join("\n");
1442
+ throw new GpcError(
1443
+ `Listing push blocked: field(s) exceed character limits:
1444
+ ${details}`,
1445
+ "LISTING_CHAR_LIMIT_EXCEEDED",
1446
+ 1,
1447
+ "Fix the character limit violations listed above, or use --force to push anyway."
1448
+ );
1449
+ }
1450
+ }
1247
1451
  const edit = await client.edits.insert(packageName);
1248
1452
  try {
1249
1453
  if (options?.dryRun) {
@@ -1358,8 +1562,8 @@ var ALL_IMAGE_TYPES = [
1358
1562
  "tvBanner"
1359
1563
  ];
1360
1564
  async function exportImages(client, packageName, dir, options) {
1361
- const { mkdir: mkdir6, writeFile: writeFile8 } = await import("fs/promises");
1362
- const { join: join8 } = await import("path");
1565
+ const { mkdir: mkdir7, writeFile: writeFile9 } = await import("fs/promises");
1566
+ const { join: join9 } = await import("path");
1363
1567
  const edit = await client.edits.insert(packageName);
1364
1568
  try {
1365
1569
  let languages;
@@ -1390,12 +1594,12 @@ async function exportImages(client, packageName, dir, options) {
1390
1594
  const batch = tasks.slice(i, i + concurrency);
1391
1595
  const results = await Promise.all(
1392
1596
  batch.map(async (task) => {
1393
- const dirPath = join8(dir, task.language, task.imageType);
1394
- await mkdir6(dirPath, { recursive: true });
1597
+ const dirPath = join9(dir, task.language, task.imageType);
1598
+ await mkdir7(dirPath, { recursive: true });
1395
1599
  const response = await fetch(task.url);
1396
1600
  const buffer = Buffer.from(await response.arrayBuffer());
1397
- const filePath = join8(dirPath, `${task.index}.png`);
1398
- await writeFile8(filePath, buffer);
1601
+ const filePath = join9(dirPath, `${task.index}.png`);
1602
+ await writeFile9(filePath, buffer);
1399
1603
  return buffer.length;
1400
1604
  })
1401
1605
  );
@@ -1898,6 +2102,234 @@ async function publish(client, packageName, filePath, options) {
1898
2102
 
1899
2103
  // src/commands/reviews.ts
1900
2104
  import { paginateAll } from "@gpc-cli/api";
2105
+
2106
+ // src/utils/sentiment.ts
2107
+ var POSITIVE_WORDS = /* @__PURE__ */ new Set([
2108
+ "great",
2109
+ "excellent",
2110
+ "amazing",
2111
+ "awesome",
2112
+ "fantastic",
2113
+ "love",
2114
+ "good",
2115
+ "best",
2116
+ "perfect",
2117
+ "wonderful",
2118
+ "helpful",
2119
+ "easy",
2120
+ "fast",
2121
+ "smooth",
2122
+ "reliable",
2123
+ "clean",
2124
+ "beautiful",
2125
+ "intuitive",
2126
+ "works",
2127
+ "recommend",
2128
+ "useful",
2129
+ "thank",
2130
+ "thanks",
2131
+ "brilliant",
2132
+ "superb",
2133
+ "flawless",
2134
+ "outstanding",
2135
+ "delightful",
2136
+ "nice"
2137
+ ]);
2138
+ var NEGATIVE_WORDS = /* @__PURE__ */ new Set([
2139
+ "bad",
2140
+ "terrible",
2141
+ "awful",
2142
+ "horrible",
2143
+ "worst",
2144
+ "hate",
2145
+ "broken",
2146
+ "crash",
2147
+ "crashes",
2148
+ "bug",
2149
+ "bugs",
2150
+ "slow",
2151
+ "laggy",
2152
+ "freeze",
2153
+ "freezes",
2154
+ "error",
2155
+ "errors",
2156
+ "fail",
2157
+ "fails",
2158
+ "useless",
2159
+ "disappointing",
2160
+ "disappointed",
2161
+ "frustrating",
2162
+ "frustration",
2163
+ "annoying",
2164
+ "problem",
2165
+ "problems",
2166
+ "issue",
2167
+ "issues",
2168
+ "fix",
2169
+ "please",
2170
+ "not working",
2171
+ "doesn't work",
2172
+ "stopped",
2173
+ "uninstall",
2174
+ "deleted",
2175
+ "waste",
2176
+ "rubbish",
2177
+ "garbage",
2178
+ "terrible"
2179
+ ]);
2180
+ var STOP_WORDS = /* @__PURE__ */ new Set([
2181
+ "the",
2182
+ "a",
2183
+ "an",
2184
+ "and",
2185
+ "or",
2186
+ "but",
2187
+ "in",
2188
+ "on",
2189
+ "at",
2190
+ "to",
2191
+ "for",
2192
+ "of",
2193
+ "with",
2194
+ "is",
2195
+ "it",
2196
+ "this",
2197
+ "that",
2198
+ "was",
2199
+ "are",
2200
+ "be",
2201
+ "been",
2202
+ "have",
2203
+ "has",
2204
+ "had",
2205
+ "do",
2206
+ "does",
2207
+ "did",
2208
+ "will",
2209
+ "would",
2210
+ "could",
2211
+ "should",
2212
+ "may",
2213
+ "might",
2214
+ "i",
2215
+ "me",
2216
+ "my",
2217
+ "we",
2218
+ "you",
2219
+ "he",
2220
+ "she",
2221
+ "they",
2222
+ "them",
2223
+ "their",
2224
+ "its",
2225
+ "not",
2226
+ "no",
2227
+ "very",
2228
+ "so",
2229
+ "just",
2230
+ "really",
2231
+ "app",
2232
+ "application",
2233
+ "update"
2234
+ ]);
2235
+ function analyzeSentiment(text) {
2236
+ const lower = text.toLowerCase();
2237
+ const words = lower.split(/\W+/).filter(Boolean);
2238
+ let posScore = 0;
2239
+ let negScore = 0;
2240
+ for (const word of words) {
2241
+ if (POSITIVE_WORDS.has(word)) posScore++;
2242
+ if (NEGATIVE_WORDS.has(word)) negScore++;
2243
+ }
2244
+ const total = posScore + negScore;
2245
+ if (total === 0) return { score: 0, label: "neutral", magnitude: 0 };
2246
+ const score = (posScore - negScore) / total;
2247
+ const magnitude = Math.min(1, total / 10);
2248
+ const label = score > 0.1 ? "positive" : score < -0.1 ? "negative" : "neutral";
2249
+ return { score, label, magnitude };
2250
+ }
2251
+ function clusterTopics(texts) {
2252
+ const TOPIC_KEYWORDS = {
2253
+ "performance": ["slow", "lag", "laggy", "freeze", "fast", "speed", "quick", "smooth"],
2254
+ "crashes": ["crash", "crashes", "crash", "crashing", "force close", "stops", "stopped"],
2255
+ "ui/ux": ["ui", "design", "interface", "layout", "button", "screen", "menu", "navigation"],
2256
+ "battery": ["battery", "drain", "power", "charging", "drain"],
2257
+ "updates": ["update", "updated", "version", "new version", "after update"],
2258
+ "notifications": ["notification", "notifications", "alert", "alerts", "push"],
2259
+ "login/auth": ["login", "sign in", "logout", "password", "account", "auth"],
2260
+ "feature requests": ["please add", "would be nice", "missing", "need", "wish", "want"],
2261
+ "bugs": ["bug", "bugs", "issue", "error", "problem", "glitch", "broken"],
2262
+ "pricing": ["price", "pricing", "expensive", "cheap", "subscription", "pay", "cost", "free"]
2263
+ };
2264
+ const clusterMap = /* @__PURE__ */ new Map();
2265
+ for (const text of texts) {
2266
+ const lower = text.toLowerCase();
2267
+ const sentiment = analyzeSentiment(text);
2268
+ for (const [topic, keywords] of Object.entries(TOPIC_KEYWORDS)) {
2269
+ if (keywords.some((kw) => lower.includes(kw))) {
2270
+ const existing = clusterMap.get(topic) ?? { count: 0, totalScore: 0 };
2271
+ clusterMap.set(topic, {
2272
+ count: existing.count + 1,
2273
+ totalScore: existing.totalScore + sentiment.score
2274
+ });
2275
+ }
2276
+ }
2277
+ }
2278
+ return Array.from(clusterMap.entries()).map(([topic, { count, totalScore }]) => ({
2279
+ topic,
2280
+ count,
2281
+ avgScore: count > 0 ? Math.round(totalScore / count * 100) / 100 : 0
2282
+ })).filter((c) => c.count > 0).sort((a, b) => b.count - a.count);
2283
+ }
2284
+ function keywordFrequency(texts, topN = 20) {
2285
+ const freq = /* @__PURE__ */ new Map();
2286
+ for (const text of texts) {
2287
+ const words = text.toLowerCase().split(/\W+/).filter((w) => w.length > 3 && !STOP_WORDS.has(w));
2288
+ for (const word of words) {
2289
+ freq.set(word, (freq.get(word) ?? 0) + 1);
2290
+ }
2291
+ }
2292
+ return Array.from(freq.entries()).map(([word, count]) => ({ word, count })).sort((a, b) => b.count - a.count).slice(0, topN);
2293
+ }
2294
+ function analyzeReviews(reviews) {
2295
+ if (reviews.length === 0) {
2296
+ return {
2297
+ totalReviews: 0,
2298
+ avgRating: 0,
2299
+ sentiment: { positive: 0, negative: 0, neutral: 0, avgScore: 0 },
2300
+ topics: [],
2301
+ keywords: [],
2302
+ ratingDistribution: {}
2303
+ };
2304
+ }
2305
+ const texts = reviews.map((r) => r.text);
2306
+ const sentiments = texts.map((t) => analyzeSentiment(t));
2307
+ const positive = sentiments.filter((s) => s.label === "positive").length;
2308
+ const negative = sentiments.filter((s) => s.label === "negative").length;
2309
+ const neutral = sentiments.filter((s) => s.label === "neutral").length;
2310
+ const avgScore = sentiments.reduce((sum, s) => sum + s.score, 0) / sentiments.length;
2311
+ const ratings = reviews.map((r) => r.rating).filter((r) => r !== void 0);
2312
+ const avgRating = ratings.length > 0 ? ratings.reduce((a, b) => a + b, 0) / ratings.length : 0;
2313
+ const ratingDistribution = {};
2314
+ for (const r of ratings) {
2315
+ ratingDistribution[r] = (ratingDistribution[r] ?? 0) + 1;
2316
+ }
2317
+ return {
2318
+ totalReviews: reviews.length,
2319
+ avgRating: Math.round(avgRating * 100) / 100,
2320
+ sentiment: {
2321
+ positive,
2322
+ negative,
2323
+ neutral,
2324
+ avgScore: Math.round(avgScore * 100) / 100
2325
+ },
2326
+ topics: clusterTopics(texts),
2327
+ keywords: keywordFrequency(texts),
2328
+ ratingDistribution
2329
+ };
2330
+ }
2331
+
2332
+ // src/commands/reviews.ts
1901
2333
  async function listReviews(client, packageName, options) {
1902
2334
  const apiOptions = {};
1903
2335
  if (options?.translationLanguage) apiOptions.translationLanguage = options.translationLanguage;
@@ -2007,163 +2439,16 @@ function csvEscape(value) {
2007
2439
  }
2008
2440
  return value;
2009
2441
  }
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
- }
2442
+ async function analyzeReviews2(client, packageName, options) {
2443
+ const reviews = await listReviews(client, packageName, options);
2444
+ const items = reviews.map((r) => {
2445
+ const uc = r.comments?.[0]?.userComment;
2446
+ return {
2447
+ text: uc?.text ?? "",
2448
+ rating: uc?.starRating
2449
+ };
2124
2450
  });
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
- };
2451
+ return analyzeReviews(items);
2167
2452
  }
2168
2453
 
2169
2454
  // src/commands/subscriptions.ts
@@ -2180,7 +2465,9 @@ function sanitizeSubscription(data) {
2180
2465
  delete cleaned["archived"];
2181
2466
  if (cleaned.basePlans) {
2182
2467
  cleaned.basePlans = cleaned.basePlans.map((bp) => {
2183
- const { state: _s, archived: _a, ...cleanBp } = bp;
2468
+ const { state: _state, archived: _archived, ...cleanBp } = bp;
2469
+ void _state;
2470
+ void _archived;
2184
2471
  if (cleanBp.regionalConfigs) {
2185
2472
  cleanBp.regionalConfigs = cleanBp.regionalConfigs.map((rc) => ({
2186
2473
  ...rc,
@@ -2193,7 +2480,8 @@ function sanitizeSubscription(data) {
2193
2480
  return cleaned;
2194
2481
  }
2195
2482
  function sanitizeOffer(data) {
2196
- const { state: _s, ...cleaned } = data;
2483
+ const { state: _state2, ...cleaned } = data;
2484
+ void _state2;
2197
2485
  delete cleaned["archived"];
2198
2486
  return cleaned;
2199
2487
  }
@@ -2211,7 +2499,7 @@ function autoFixProrationMode(data) {
2211
2499
  for (const bp of data.basePlans) {
2212
2500
  const mode = bp.autoRenewingBasePlanType?.prorationMode;
2213
2501
  if (mode && !mode.startsWith(PRORATION_MODE_PREFIX)) {
2214
- bp.autoRenewingBasePlanType.prorationMode = `${PRORATION_MODE_PREFIX}${mode}`;
2502
+ if (bp.autoRenewingBasePlanType) bp.autoRenewingBasePlanType.prorationMode = `${PRORATION_MODE_PREFIX}${mode}`;
2215
2503
  }
2216
2504
  if (bp.autoRenewingBasePlanType?.prorationMode) {
2217
2505
  const fullMode = bp.autoRenewingBasePlanType.prorationMode;
@@ -2399,19 +2687,323 @@ async function deactivateOffer(client, packageName, productId, basePlanId, offer
2399
2687
  validateSku(productId);
2400
2688
  return client.subscriptions.deactivateOffer(packageName, productId, basePlanId, offerId);
2401
2689
  }
2690
+ async function getSubscriptionAnalytics(client, packageName) {
2691
+ validatePackageName(packageName);
2692
+ const { items: subs } = await paginateAll2(async (pageToken) => {
2693
+ const response = await client.subscriptions.list(packageName, {
2694
+ pageToken,
2695
+ pageSize: 100
2696
+ });
2697
+ return {
2698
+ items: response.subscriptions || [],
2699
+ nextPageToken: response.nextPageToken
2700
+ };
2701
+ });
2702
+ let activeCount = 0;
2703
+ let activeBasePlans = 0;
2704
+ let trialBasePlans = 0;
2705
+ let pausedBasePlans = 0;
2706
+ let canceledBasePlans = 0;
2707
+ let totalOffers = 0;
2708
+ const byProductId = [];
2709
+ for (const sub of subs) {
2710
+ const state = sub["state"];
2711
+ if (state === "ACTIVE") activeCount++;
2712
+ const basePlans = sub.basePlans ?? [];
2713
+ let subOfferCount = 0;
2714
+ for (const bp of basePlans) {
2715
+ const bpState = bp["state"];
2716
+ if (bpState === "ACTIVE") activeBasePlans++;
2717
+ else if (bpState === "DRAFT") trialBasePlans++;
2718
+ else if (bpState === "INACTIVE") pausedBasePlans++;
2719
+ else if (bpState === "PREPUBLISHED") canceledBasePlans++;
2720
+ }
2721
+ for (const bp of basePlans) {
2722
+ try {
2723
+ const offersResp = await client.subscriptions.listOffers(packageName, sub.productId, bp.basePlanId);
2724
+ const offers = offersResp.subscriptionOffers ?? [];
2725
+ subOfferCount += offers.length;
2726
+ totalOffers += offers.length;
2727
+ } catch {
2728
+ }
2729
+ }
2730
+ byProductId.push({
2731
+ productId: sub.productId,
2732
+ state: sub["state"] ?? "UNKNOWN",
2733
+ basePlanCount: basePlans.length,
2734
+ offerCount: subOfferCount
2735
+ });
2736
+ }
2737
+ return {
2738
+ totalSubscriptions: subs.length,
2739
+ activeCount,
2740
+ activeBasePlans,
2741
+ trialBasePlans,
2742
+ pausedBasePlans,
2743
+ canceledBasePlans,
2744
+ offerCount: totalOffers,
2745
+ byProductId
2746
+ };
2747
+ }
2402
2748
 
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
- });
2749
+ // src/commands/vitals.ts
2750
+ var METRIC_SET_METRICS = {
2751
+ crashRateMetricSet: ["crashRate", "userPerceivedCrashRate", "distinctUsers"],
2752
+ anrRateMetricSet: ["anrRate", "userPerceivedAnrRate", "distinctUsers"],
2753
+ slowStartRateMetricSet: ["slowStartRate", "distinctUsers"],
2754
+ slowRenderingRateMetricSet: ["slowRenderingRate", "distinctUsers"],
2755
+ excessiveWakeupRateMetricSet: ["excessiveWakeupRate", "distinctUsers"],
2756
+ stuckBackgroundWakelockRateMetricSet: ["stuckBackgroundWakelockRate", "distinctUsers"],
2757
+ errorCountMetricSet: ["errorReportCount", "distinctUsers"]
2758
+ };
2759
+ function buildQuery(metricSet, options) {
2760
+ const metrics = METRIC_SET_METRICS[metricSet] ?? ["errorReportCount", "distinctUsers"];
2761
+ const days = options?.days ?? 30;
2762
+ const DAY_MS = 24 * 60 * 60 * 1e3;
2763
+ const end = new Date(Date.now() - DAY_MS);
2764
+ const start = new Date(Date.now() - DAY_MS - days * DAY_MS);
2765
+ const query = {
2766
+ metrics,
2767
+ timelineSpec: {
2768
+ aggregationPeriod: options?.aggregation ?? "DAILY",
2769
+ startTime: {
2770
+ year: start.getUTCFullYear(),
2771
+ month: start.getUTCMonth() + 1,
2772
+ day: start.getUTCDate()
2773
+ },
2774
+ endTime: {
2775
+ year: end.getUTCFullYear(),
2776
+ month: end.getUTCMonth() + 1,
2777
+ day: end.getUTCDate()
2778
+ }
2779
+ }
2780
+ };
2781
+ if (options?.dimension) {
2782
+ query.dimensions = [options.dimension];
2783
+ }
2784
+ return query;
2785
+ }
2786
+ async function queryMetric(reporting, packageName, metricSet, options) {
2787
+ const query = buildQuery(metricSet, options);
2788
+ return reporting.queryMetricSet(packageName, metricSet, query);
2789
+ }
2790
+ async function getVitalsOverview(reporting, packageName) {
2791
+ const metricSets = [
2792
+ ["crashRateMetricSet", "crashRate"],
2793
+ ["anrRateMetricSet", "anrRate"],
2794
+ ["slowStartRateMetricSet", "slowStartRate"],
2795
+ ["slowRenderingRateMetricSet", "slowRenderingRate"],
2796
+ ["excessiveWakeupRateMetricSet", "excessiveWakeupRate"],
2797
+ ["stuckBackgroundWakelockRateMetricSet", "stuckWakelockRate"]
2798
+ ];
2799
+ const results = await Promise.allSettled(
2800
+ metricSets.map(
2801
+ ([metric]) => reporting.queryMetricSet(packageName, metric, buildQuery(metric))
2802
+ )
2803
+ );
2804
+ const overview = {};
2805
+ for (let i = 0; i < metricSets.length; i++) {
2806
+ const entry = metricSets[i];
2807
+ if (!entry) continue;
2808
+ const key = entry[1];
2809
+ const result = results[i];
2810
+ if (!result) continue;
2811
+ if (result.status === "fulfilled") {
2812
+ overview[key] = result.value.rows || [];
2813
+ }
2814
+ }
2815
+ return overview;
2816
+ }
2817
+ async function getVitalsCrashes(reporting, packageName, options) {
2818
+ return queryMetric(reporting, packageName, "crashRateMetricSet", options);
2819
+ }
2820
+ async function getVitalsAnr(reporting, packageName, options) {
2821
+ return queryMetric(reporting, packageName, "anrRateMetricSet", options);
2822
+ }
2823
+ async function getVitalsStartup(reporting, packageName, options) {
2824
+ return queryMetric(reporting, packageName, "slowStartRateMetricSet", options);
2825
+ }
2826
+ async function getVitalsRendering(reporting, packageName, options) {
2827
+ return queryMetric(reporting, packageName, "slowRenderingRateMetricSet", options);
2828
+ }
2829
+ async function getVitalsBattery(reporting, packageName, options) {
2830
+ return queryMetric(reporting, packageName, "excessiveWakeupRateMetricSet", options);
2831
+ }
2832
+ async function getVitalsMemory(reporting, packageName, options) {
2833
+ return queryMetric(reporting, packageName, "stuckBackgroundWakelockRateMetricSet", options);
2834
+ }
2835
+ async function getVitalsLmk(reporting, packageName, options) {
2836
+ return queryMetric(reporting, packageName, "stuckBackgroundWakelockRateMetricSet", {
2837
+ ...options,
2838
+ aggregation: "DAILY"
2839
+ });
2840
+ }
2841
+ async function getVitalsAnomalies(reporting, packageName) {
2842
+ return reporting.getAnomalies(packageName);
2843
+ }
2844
+ async function searchVitalsErrors(reporting, packageName, options) {
2845
+ return reporting.searchErrorIssues(packageName, options?.filter, options?.maxResults);
2846
+ }
2847
+ async function compareVitalsTrend(reporting, packageName, metricSet, days = 7) {
2848
+ const DAY_MS = 24 * 60 * 60 * 1e3;
2849
+ const nowMs = Date.now();
2850
+ const baseMs = nowMs - 2 * DAY_MS;
2851
+ const currentEnd = new Date(baseMs);
2852
+ const currentStart = new Date(baseMs - days * DAY_MS);
2853
+ const previousEnd = new Date(baseMs - days * DAY_MS - DAY_MS);
2854
+ const previousStart = new Date(baseMs - days * DAY_MS - DAY_MS - days * DAY_MS);
2855
+ const metrics = METRIC_SET_METRICS[metricSet] ?? ["errorReportCount", "distinctUsers"];
2856
+ const toApiDate2 = (d) => ({
2857
+ year: d.getUTCFullYear(),
2858
+ month: d.getUTCMonth() + 1,
2859
+ day: d.getUTCDate()
2860
+ });
2861
+ const makeQuery = (start, end) => ({
2862
+ metrics,
2863
+ timelineSpec: {
2864
+ aggregationPeriod: "DAILY",
2865
+ startTime: toApiDate2(start),
2866
+ endTime: toApiDate2(end)
2867
+ }
2868
+ });
2869
+ const [currentResult, previousResult] = await Promise.all([
2870
+ reporting.queryMetricSet(packageName, metricSet, makeQuery(currentStart, currentEnd)),
2871
+ reporting.queryMetricSet(packageName, metricSet, makeQuery(previousStart, previousEnd))
2872
+ ]);
2873
+ const extractAvg = (rows) => {
2874
+ if (!rows || rows.length === 0) return void 0;
2875
+ const values = rows.map((r) => {
2876
+ const keys = Object.keys(r.metrics);
2877
+ const first = keys[0];
2878
+ return first ? Number(r.metrics[first]?.decimalValue?.value) : NaN;
2879
+ }).filter((v) => !isNaN(v));
2880
+ if (values.length === 0) return void 0;
2881
+ return values.reduce((a, b) => a + b, 0) / values.length;
2882
+ };
2883
+ const current = extractAvg(currentResult.rows);
2884
+ const previous = extractAvg(previousResult.rows);
2885
+ let changePercent;
2886
+ let direction = "unknown";
2887
+ if (current !== void 0 && previous !== void 0 && previous !== 0) {
2888
+ changePercent = (current - previous) / previous * 100;
2889
+ if (Math.abs(changePercent) < 1) {
2890
+ direction = "unchanged";
2891
+ } else if (changePercent < 0) {
2892
+ direction = "improved";
2893
+ } else {
2894
+ direction = "degraded";
2895
+ }
2896
+ }
2897
+ return {
2898
+ metric: metricSet,
2899
+ current,
2900
+ previous,
2901
+ changePercent: changePercent !== void 0 ? Math.round(changePercent * 10) / 10 : void 0,
2902
+ direction
2903
+ };
2904
+ }
2905
+ function checkThreshold(value, threshold) {
2906
+ return {
2907
+ breached: value !== void 0 && value > threshold,
2908
+ value,
2909
+ threshold
2910
+ };
2911
+ }
2912
+ async function compareVersionVitals(reporting, packageName, v1, v2, options) {
2913
+ const days = options?.days ?? 30;
2914
+ const metricSets = [
2915
+ ["crashRateMetricSet", "crashRate"],
2916
+ ["anrRateMetricSet", "anrRate"],
2917
+ ["slowStartRateMetricSet", "slowStartRate"],
2918
+ ["slowRenderingRateMetricSet", "slowRenderingRate"]
2919
+ ];
2920
+ const results = await Promise.allSettled(
2921
+ metricSets.map(
2922
+ ([ms]) => queryMetric(reporting, packageName, ms, { dimension: "versionCode", days })
2923
+ )
2924
+ );
2925
+ const row1 = { versionCode: v1 };
2926
+ const row2 = { versionCode: v2 };
2927
+ for (let i = 0; i < metricSets.length; i++) {
2928
+ const entry = metricSets[i];
2929
+ const result = results[i];
2930
+ if (!entry || !result || result.status !== "fulfilled") continue;
2931
+ const key = entry[1];
2932
+ const rows = result.value.rows ?? [];
2933
+ const extractAvgForVersion = (vc) => {
2934
+ const matching = rows.filter((r) => {
2935
+ const dims = r.dimensions;
2936
+ return dims?.some((d) => d["stringValue"] === vc) ?? false;
2937
+ });
2938
+ if (matching.length === 0) return void 0;
2939
+ const values = matching.map((r) => {
2940
+ const firstKey = Object.keys(r.metrics)[0];
2941
+ return firstKey ? Number(r.metrics[firstKey]?.decimalValue?.value) : NaN;
2942
+ }).filter((v) => !isNaN(v));
2943
+ if (values.length === 0) return void 0;
2944
+ return values.reduce((a, b) => a + b, 0) / values.length;
2945
+ };
2946
+ row1[key] = extractAvgForVersion(v1);
2947
+ row2[key] = extractAvgForVersion(v2);
2948
+ }
2949
+ const regressions = [];
2950
+ for (const [, key] of metricSets) {
2951
+ const val1 = row1[key];
2952
+ const val2 = row2[key];
2953
+ if (val1 !== void 0 && val2 !== void 0 && val2 > val1 * 1.05) {
2954
+ regressions.push(key);
2955
+ }
2956
+ }
2957
+ return { v1: row1, v2: row2, regressions };
2958
+ }
2959
+ function watchVitalsWithAutoHalt(reporting, packageName, options) {
2960
+ const {
2961
+ intervalMs = 5 * 60 * 1e3,
2962
+ threshold,
2963
+ metricSet = "crashRateMetricSet",
2964
+ onHalt,
2965
+ onPoll
2966
+ } = options;
2967
+ let stopped = false;
2968
+ let haltTriggered = false;
2969
+ const poll = async () => {
2970
+ if (stopped) return;
2971
+ try {
2972
+ const result = await queryMetric(reporting, packageName, metricSet, { days: 1 });
2973
+ const latestRow = result.rows?.[result.rows.length - 1];
2974
+ const firstMetric = latestRow?.metrics ? Object.keys(latestRow.metrics)[0] : void 0;
2975
+ const value = firstMetric ? Number(latestRow?.metrics[firstMetric]?.decimalValue?.value) : void 0;
2976
+ const breached = value !== void 0 && value > threshold;
2977
+ onPoll?.(value, breached);
2978
+ if (breached && !haltTriggered && onHalt) {
2979
+ haltTriggered = true;
2980
+ await onHalt(value);
2981
+ }
2982
+ } catch {
2983
+ }
2984
+ if (!stopped) {
2985
+ timerId = setTimeout(poll, intervalMs);
2986
+ }
2987
+ };
2988
+ let timerId = setTimeout(poll, 0);
2989
+ return () => {
2990
+ stopped = true;
2991
+ clearTimeout(timerId);
2992
+ };
2993
+ }
2994
+
2995
+ // src/commands/iap.ts
2996
+ import { readdir as readdir4, readFile as readFile5 } from "fs/promises";
2997
+ import { join as join4 } from "path";
2998
+ import { paginateAll as paginateAll3 } from "@gpc-cli/api";
2999
+ async function listInAppProducts(client, packageName, options) {
3000
+ if (options?.limit || options?.nextPage) {
3001
+ const result = await paginateAll3(
3002
+ async (pageToken) => {
3003
+ const resp = await client.inappproducts.list(packageName, {
3004
+ token: pageToken,
3005
+ maxResults: options?.maxResults
3006
+ });
2415
3007
  return {
2416
3008
  items: resp.inappproduct || [],
2417
3009
  nextPageToken: resp.tokenPagination?.nextPageToken
@@ -2592,6 +3184,10 @@ async function revokeSubscriptionPurchase(client, packageName, token) {
2592
3184
  validatePackageName(packageName);
2593
3185
  return client.purchases.revokeSubscriptionV2(packageName, token);
2594
3186
  }
3187
+ async function refundSubscriptionV2(client, packageName, token) {
3188
+ validatePackageName(packageName);
3189
+ return client.purchases.refundSubscriptionV2(packageName, token);
3190
+ }
2595
3191
  async function listVoidedPurchases(client, packageName, options) {
2596
3192
  validatePackageName(packageName);
2597
3193
  if (options?.limit || options?.nextPage) {
@@ -2666,8 +3262,8 @@ function isStatsReportType(type) {
2666
3262
  function isValidReportType(type) {
2667
3263
  return FINANCIAL_REPORT_TYPES.has(type) || STATS_REPORT_TYPES.has(type);
2668
3264
  }
2669
- function isValidStatsDimension(dim) {
2670
- return VALID_DIMENSIONS.has(dim);
3265
+ function isValidStatsDimension(dim2) {
3266
+ return VALID_DIMENSIONS.has(dim2);
2671
3267
  }
2672
3268
  function parseMonth(monthStr) {
2673
3269
  const match = /^(\d{4})-(\d{2})$/.exec(monthStr);
@@ -2766,6 +3362,26 @@ function parseGrantArg(grantStr) {
2766
3362
  return { packageName, appLevelPermissions: perms };
2767
3363
  }
2768
3364
 
3365
+ // src/commands/grants.ts
3366
+ async function listGrants(client, developerId, email) {
3367
+ const result = await client.grants.list(developerId, email);
3368
+ return result.grants || [];
3369
+ }
3370
+ async function createGrant(client, developerId, email, packageName, permissions) {
3371
+ return client.grants.create(developerId, email, {
3372
+ packageName,
3373
+ appLevelPermissions: permissions
3374
+ });
3375
+ }
3376
+ async function updateGrant(client, developerId, email, packageName, permissions) {
3377
+ return client.grants.patch(developerId, email, packageName, {
3378
+ appLevelPermissions: permissions
3379
+ }, "appLevelPermissions");
3380
+ }
3381
+ async function deleteGrant(client, developerId, email, packageName) {
3382
+ return client.grants.delete(developerId, email, packageName);
3383
+ }
3384
+
2769
3385
  // src/commands/testers.ts
2770
3386
  import { readFile as readFile6 } from "fs/promises";
2771
3387
  async function listTesters(client, packageName, track) {
@@ -2843,7 +3459,7 @@ var DEFAULT_MAX_LENGTH = 500;
2843
3459
  function parseConventionalCommit(subject) {
2844
3460
  const match = subject.match(/^(\w+)(?:\([^)]*\))?:\s*(.+)$/);
2845
3461
  if (match) {
2846
- return { type: match[1], message: match[2].trim() };
3462
+ return { type: match[1] ?? "other", message: (match[2] ?? "").trim() };
2847
3463
  }
2848
3464
  return { type: "other", message: subject.trim() };
2849
3465
  }
@@ -2854,7 +3470,7 @@ function formatNotes(commits, maxLength) {
2854
3470
  if (!groups.has(header)) {
2855
3471
  groups.set(header, []);
2856
3472
  }
2857
- groups.get(header).push(commit.message);
3473
+ groups.get(header)?.push(commit.message);
2858
3474
  }
2859
3475
  const order = ["New", "Fixed", "Improved", "Changes"];
2860
3476
  const sections = [];
@@ -3266,190 +3882,182 @@ function createSpinner(message) {
3266
3882
  };
3267
3883
  }
3268
3884
 
3269
- // src/utils/safe-path.ts
3270
- import { resolve, normalize } from "path";
3271
- function safePath(userPath) {
3272
- return resolve(normalize(userPath));
3885
+ // src/utils/train-state.ts
3886
+ import { mkdir as mkdir3, readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
3887
+ import { join as join5 } from "path";
3888
+ import { getCacheDir } from "@gpc-cli/config";
3889
+ function stateFile(packageName) {
3890
+ return join5(getCacheDir(), `train-${packageName}.json`);
3273
3891
  }
3274
- function safePathWithin(userPath, baseDir) {
3275
- const resolved = safePath(userPath);
3276
- const base = safePath(baseDir);
3277
- if (!resolved.startsWith(base + "/") && resolved !== base) {
3278
- throw new GpcError(
3279
- `Path "${userPath}" resolves outside the expected directory "${baseDir}"`,
3280
- "PATH_TRAVERSAL",
3281
- 2,
3282
- "The path must stay within the target directory. Remove any ../ segments or use an absolute path within the expected directory."
3283
- );
3892
+ async function readTrainState(packageName) {
3893
+ const path = stateFile(packageName);
3894
+ try {
3895
+ const raw = await readFile8(path, "utf-8");
3896
+ return JSON.parse(raw);
3897
+ } catch {
3898
+ return null;
3899
+ }
3900
+ }
3901
+ async function writeTrainState(packageName, state) {
3902
+ const path = stateFile(packageName);
3903
+ const dir = path.substring(0, path.lastIndexOf("/"));
3904
+ await mkdir3(dir, { recursive: true });
3905
+ await writeFile4(path, JSON.stringify(state, null, 2), "utf-8");
3906
+ }
3907
+ async function clearTrainState(packageName) {
3908
+ const { unlink } = await import("fs/promises");
3909
+ const path = stateFile(packageName);
3910
+ await unlink(path).catch(() => {
3911
+ });
3912
+ }
3913
+ function parseDuration2(s) {
3914
+ const match = /^(\d+)(d|h|m)$/.exec(s.trim());
3915
+ if (!match) return 0;
3916
+ const n = parseInt(match[1] ?? "0", 10);
3917
+ switch (match[2]) {
3918
+ case "d":
3919
+ return n * 24 * 60 * 60 * 1e3;
3920
+ case "h":
3921
+ return n * 60 * 60 * 1e3;
3922
+ case "m":
3923
+ return n * 60 * 1e3;
3924
+ default:
3925
+ return 0;
3284
3926
  }
3285
- return resolved;
3286
3927
  }
3287
3928
 
3288
- // src/utils/sort.ts
3289
- function getNestedValue(obj, path) {
3290
- const parts = path.split(".");
3291
- let current = obj;
3292
- for (const part of parts) {
3293
- if (current === null || current === void 0 || typeof current !== "object") {
3294
- return void 0;
3929
+ // src/commands/train.ts
3930
+ async function startTrain(apiClient, packageName, config, options) {
3931
+ const existing = await readTrainState(packageName);
3932
+ if (existing && existing.status === "running" && !options?.force) {
3933
+ return existing;
3934
+ }
3935
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3936
+ const state = {
3937
+ packageName,
3938
+ status: "running",
3939
+ currentStage: 0,
3940
+ startedAt: now,
3941
+ updatedAt: now,
3942
+ stages: config.stages.map((s, i) => ({
3943
+ ...s,
3944
+ scheduledAt: i === 0 ? now : void 0
3945
+ })),
3946
+ gates: config.gates
3947
+ };
3948
+ await writeTrainState(packageName, state);
3949
+ await executeStage(apiClient, packageName, state, 0);
3950
+ return state;
3951
+ }
3952
+ async function getTrainStatus(packageName) {
3953
+ return readTrainState(packageName);
3954
+ }
3955
+ async function pauseTrain(packageName) {
3956
+ const state = await readTrainState(packageName);
3957
+ if (!state || state.status !== "running") return state;
3958
+ state.status = "paused";
3959
+ state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3960
+ await writeTrainState(packageName, state);
3961
+ return state;
3962
+ }
3963
+ async function abortTrain(packageName) {
3964
+ await clearTrainState(packageName);
3965
+ }
3966
+ async function advanceTrain(apiClient, reportingClient, packageName) {
3967
+ const state = await readTrainState(packageName);
3968
+ if (!state || state.status !== "running") return state;
3969
+ const nextStage = state.currentStage + 1;
3970
+ if (nextStage >= state.stages.length) {
3971
+ state.status = "completed";
3972
+ state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3973
+ await writeTrainState(packageName, state);
3974
+ return state;
3975
+ }
3976
+ const nextStageConfig = state.stages[nextStage];
3977
+ if (!nextStageConfig) return state;
3978
+ if (nextStageConfig.after) {
3979
+ const delayMs = parseDuration2(nextStageConfig.after);
3980
+ const currentStageConfig = state.stages[state.currentStage];
3981
+ const executedAt = currentStageConfig?.executedAt;
3982
+ if (executedAt && delayMs > 0) {
3983
+ const elapsed = Date.now() - new Date(executedAt).getTime();
3984
+ if (elapsed < delayMs) {
3985
+ const readyAt = new Date(new Date(executedAt).getTime() + delayMs).toISOString();
3986
+ nextStageConfig.scheduledAt = readyAt;
3987
+ await writeTrainState(packageName, state);
3988
+ return state;
3989
+ }
3295
3990
  }
3296
- current = current[part];
3297
3991
  }
3298
- return current;
3992
+ if (state.gates) {
3993
+ if (state.gates.crashes?.max !== void 0) {
3994
+ const result = await getVitalsCrashes(reportingClient, packageName, { days: 1 });
3995
+ const latestRow = result.rows?.[result.rows.length - 1];
3996
+ const firstMetric = latestRow?.metrics ? Object.keys(latestRow.metrics)[0] : void 0;
3997
+ const value = firstMetric ? Number(latestRow?.metrics[firstMetric]?.decimalValue?.value) : void 0;
3998
+ if (value !== void 0 && value > state.gates.crashes.max / 100) {
3999
+ state.status = "paused";
4000
+ state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
4001
+ await writeTrainState(packageName, state);
4002
+ throw new Error(
4003
+ `Crash gate failed: ${(value * 100).toFixed(3)}% > max ${state.gates.crashes.max}%. Train paused.`
4004
+ );
4005
+ }
4006
+ }
4007
+ if (state.gates.anr?.max !== void 0) {
4008
+ const result = await getVitalsAnr(reportingClient, packageName, { days: 1 });
4009
+ const latestRow = result.rows?.[result.rows.length - 1];
4010
+ const firstMetric = latestRow?.metrics ? Object.keys(latestRow.metrics)[0] : void 0;
4011
+ const value = firstMetric ? Number(latestRow?.metrics[firstMetric]?.decimalValue?.value) : void 0;
4012
+ if (value !== void 0 && value > state.gates.anr.max / 100) {
4013
+ state.status = "paused";
4014
+ state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
4015
+ await writeTrainState(packageName, state);
4016
+ throw new Error(
4017
+ `ANR gate failed: ${(value * 100).toFixed(3)}% > max ${state.gates.anr.max}%. Train paused.`
4018
+ );
4019
+ }
4020
+ }
4021
+ }
4022
+ await executeStage(apiClient, packageName, state, nextStage);
4023
+ return state;
3299
4024
  }
3300
- function compare(a, b) {
3301
- if (a === void 0 && b === void 0) return 0;
3302
- if (a === void 0) return 1;
3303
- if (b === void 0) return -1;
3304
- if (typeof a === "number" && typeof b === "number") {
3305
- return a - b;
3306
- }
3307
- return String(a).localeCompare(String(b));
3308
- }
3309
- function sortResults(items, sortSpec) {
3310
- if (!sortSpec || items.length === 0) {
3311
- return items;
3312
- }
3313
- const descending = sortSpec.startsWith("-");
3314
- const field = descending ? sortSpec.slice(1) : sortSpec;
3315
- const hasField = items.some((item) => getNestedValue(item, field) !== void 0);
3316
- if (!hasField) {
3317
- return items;
3318
- }
3319
- const sorted = [...items].sort((a, b) => {
3320
- const aVal = getNestedValue(a, field);
3321
- const bVal = getNestedValue(b, field);
3322
- const result = compare(aVal, bVal);
3323
- return descending ? -result : result;
3324
- });
3325
- return sorted;
4025
+ async function executeStage(apiClient, packageName, state, stageIndex) {
4026
+ const stage = state.stages[stageIndex];
4027
+ if (!stage) throw new Error(`Stage ${stageIndex} not found`);
4028
+ const rolloutFraction = stage.rollout / 100;
4029
+ await updateRollout(apiClient, packageName, stage.track, "increase", rolloutFraction);
4030
+ stage.executedAt = (/* @__PURE__ */ new Date()).toISOString();
4031
+ state.currentStage = stageIndex;
4032
+ state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
4033
+ await writeTrainState(packageName, state);
3326
4034
  }
3327
4035
 
3328
- // src/commands/plugin-scaffold.ts
3329
- import { mkdir as mkdir3, writeFile as writeFile4 } from "fs/promises";
3330
- import { join as join5 } from "path";
3331
- async function scaffoldPlugin(options) {
3332
- const { name, dir, description = `GPC plugin: ${name}` } = options;
3333
- const pluginName = name.startsWith("gpc-plugin-") ? name : `gpc-plugin-${name}`;
3334
- 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 });
3339
- const files = [];
3340
- const pkg = {
3341
- name: pluginName,
3342
- version: "0.1.0",
3343
- description,
3344
- type: "module",
3345
- main: "./dist/index.js",
3346
- types: "./dist/index.d.ts",
3347
- exports: {
3348
- ".": {
3349
- import: "./dist/index.js",
3350
- types: "./dist/index.d.ts"
3351
- }
3352
- },
3353
- files: ["dist"],
3354
- scripts: {
3355
- build: "tsup src/index.ts --format esm --dts",
3356
- dev: "tsup src/index.ts --format esm --dts --watch",
3357
- test: "vitest run",
3358
- "test:watch": "vitest"
3359
- },
3360
- keywords: ["gpc", "gpc-plugin", "google-play"],
3361
- license: "MIT",
3362
- peerDependencies: {
3363
- "@gpc-cli/plugin-sdk": ">=0.8.0"
3364
- },
3365
- devDependencies: {
3366
- "@gpc-cli/plugin-sdk": "^0.8.0",
3367
- tsup: "^8.0.0",
3368
- typescript: "^5.0.0",
3369
- vitest: "^3.0.0"
3370
- }
3371
- };
3372
- await writeFile4(join5(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
3373
- files.push("package.json");
3374
- const tsconfig = {
3375
- compilerOptions: {
3376
- target: "ES2022",
3377
- module: "ESNext",
3378
- moduleResolution: "bundler",
3379
- declaration: true,
3380
- strict: true,
3381
- esModuleInterop: true,
3382
- skipLibCheck: true,
3383
- outDir: "./dist",
3384
- rootDir: "./src"
3385
- },
3386
- include: ["src"]
3387
- };
3388
- await writeFile4(join5(dir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2) + "\n");
3389
- files.push("tsconfig.json");
3390
- const srcContent = `import { definePlugin } from "@gpc-cli/plugin-sdk";
3391
- import type { CommandEvent, CommandResult } from "@gpc-cli/plugin-sdk";
3392
-
3393
- export const plugin = definePlugin({
3394
- name: "${pluginName}",
3395
- version: "0.1.0",
3396
-
3397
- register(hooks) {
3398
- hooks.beforeCommand(async (event: CommandEvent) => {
3399
- // Called before every gpc command
3400
- // Example: log command usage, validate prerequisites, etc.
3401
- });
3402
-
3403
- hooks.afterCommand(async (event: CommandEvent, result: CommandResult) => {
3404
- // Called after every successful gpc command
3405
- // Example: send notifications, update dashboards, etc.
3406
- });
3407
-
3408
- // Uncomment to add custom commands:
3409
- // hooks.registerCommands((registry) => {
3410
- // registry.add({
3411
- // name: "${shortName}",
3412
- // description: "${description}",
3413
- // action: async (args, opts) => {
3414
- // console.log("Hello from ${pluginName}!");
3415
- // },
3416
- // });
3417
- // });
3418
- },
3419
- });
3420
- `;
3421
- await writeFile4(join5(srcDir, "index.ts"), srcContent);
3422
- files.push("src/index.ts");
3423
- const testContent = `import { describe, it, expect, vi } from "vitest";
3424
- import { plugin } from "../src/index";
3425
-
3426
- describe("${pluginName}", () => {
3427
- it("has correct name and version", () => {
3428
- expect(plugin.name).toBe("${pluginName}");
3429
- expect(plugin.version).toBe("0.1.0");
3430
- });
3431
-
3432
- it("registers without errors", () => {
3433
- const hooks = {
3434
- beforeCommand: vi.fn(),
3435
- afterCommand: vi.fn(),
3436
- onError: vi.fn(),
3437
- beforeRequest: vi.fn(),
3438
- afterResponse: vi.fn(),
3439
- registerCommands: vi.fn(),
3440
- };
4036
+ // src/commands/games.ts
4037
+ async function listLeaderboards(client, packageName) {
4038
+ const result = await client.leaderboards.list(packageName);
4039
+ return result.items ?? [];
4040
+ }
4041
+ async function listAchievements(client, packageName) {
4042
+ const result = await client.achievements.list(packageName);
4043
+ return result.items ?? [];
4044
+ }
4045
+ async function listEvents(client, packageName) {
4046
+ const result = await client.events.list(packageName);
4047
+ return result.items ?? [];
4048
+ }
3441
4049
 
3442
- expect(() => plugin.register(hooks)).not.toThrow();
3443
- });
3444
- });
3445
- `;
3446
- await writeFile4(join5(testDir, "plugin.test.ts"), testContent);
3447
- files.push("tests/plugin.test.ts");
3448
- return { dir, files };
4050
+ // src/commands/enterprise.ts
4051
+ async function listEnterpriseApps(client, organizationId) {
4052
+ const result = await client.apps.list(organizationId);
4053
+ return result.customApps ?? [];
4054
+ }
4055
+ async function createEnterpriseApp(client, organizationId, app) {
4056
+ return client.apps.create(organizationId, app);
3449
4057
  }
3450
4058
 
3451
4059
  // src/audit.ts
3452
- import { appendFile, chmod, mkdir as mkdir4, readFile as readFile8, writeFile as writeFile5 } from "fs/promises";
4060
+ import { appendFile, chmod, mkdir as mkdir4, readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
3453
4061
  import { join as join6 } from "path";
3454
4062
  var auditDir = null;
3455
4063
  function initAudit(configDir) {
@@ -3517,7 +4125,7 @@ async function listAuditEvents(options) {
3517
4125
  const logPath = join6(auditDir, "audit.log");
3518
4126
  let content;
3519
4127
  try {
3520
- content = await readFile8(logPath, "utf-8");
4128
+ content = await readFile9(logPath, "utf-8");
3521
4129
  } catch {
3522
4130
  return [];
3523
4131
  }
@@ -3555,7 +4163,7 @@ async function clearAuditLog(options) {
3555
4163
  const logPath = join6(auditDir, "audit.log");
3556
4164
  let content;
3557
4165
  try {
3558
- content = await readFile8(logPath, "utf-8");
4166
+ content = await readFile9(logPath, "utf-8");
3559
4167
  } catch {
3560
4168
  return { deleted: 0, remaining: 0 };
3561
4169
  }
@@ -3591,6 +4199,220 @@ async function clearAuditLog(options) {
3591
4199
  return { deleted: remove.length, remaining: keep.length };
3592
4200
  }
3593
4201
 
4202
+ // src/commands/quota.ts
4203
+ var DAILY_LIMIT = 2e5;
4204
+ var MINUTE_LIMIT = 3e3;
4205
+ async function getQuotaUsage() {
4206
+ const now = Date.now();
4207
+ const startOfDay = new Date(now);
4208
+ startOfDay.setUTCHours(0, 0, 0, 0);
4209
+ const startOfMinute = new Date(now - 60 * 1e3);
4210
+ const todayEntries = await listAuditEvents({
4211
+ since: startOfDay.toISOString()
4212
+ });
4213
+ const minuteEntries = todayEntries.filter(
4214
+ (e) => new Date(e.timestamp).getTime() >= startOfMinute.getTime()
4215
+ );
4216
+ const commandCounts = /* @__PURE__ */ new Map();
4217
+ for (const entry of todayEntries) {
4218
+ commandCounts.set(entry.command, (commandCounts.get(entry.command) ?? 0) + 1);
4219
+ }
4220
+ const topCommands = Array.from(commandCounts.entries()).map(([command, count]) => ({ command, count })).sort((a, b) => b.count - a.count).slice(0, 10);
4221
+ const dailyCallsUsed = todayEntries.length;
4222
+ const minuteCallsUsed = minuteEntries.length;
4223
+ return {
4224
+ dailyCallsUsed,
4225
+ dailyCallsLimit: DAILY_LIMIT,
4226
+ dailyCallsRemaining: Math.max(0, DAILY_LIMIT - dailyCallsUsed),
4227
+ minuteCallsUsed,
4228
+ minuteCallsLimit: MINUTE_LIMIT,
4229
+ minuteCallsRemaining: Math.max(0, MINUTE_LIMIT - minuteCallsUsed),
4230
+ topCommands
4231
+ };
4232
+ }
4233
+
4234
+ // src/utils/safe-path.ts
4235
+ import { resolve, normalize } from "path";
4236
+ function safePath(userPath) {
4237
+ return resolve(normalize(userPath));
4238
+ }
4239
+ function safePathWithin(userPath, baseDir) {
4240
+ const resolved = safePath(userPath);
4241
+ const base = safePath(baseDir);
4242
+ if (!resolved.startsWith(base + "/") && resolved !== base) {
4243
+ throw new GpcError(
4244
+ `Path "${userPath}" resolves outside the expected directory "${baseDir}"`,
4245
+ "PATH_TRAVERSAL",
4246
+ 2,
4247
+ "The path must stay within the target directory. Remove any ../ segments or use an absolute path within the expected directory."
4248
+ );
4249
+ }
4250
+ return resolved;
4251
+ }
4252
+
4253
+ // src/utils/sort.ts
4254
+ function getNestedValue(obj, path) {
4255
+ const parts = path.split(".");
4256
+ let current = obj;
4257
+ for (const part of parts) {
4258
+ if (current === null || current === void 0 || typeof current !== "object") {
4259
+ return void 0;
4260
+ }
4261
+ current = current[part];
4262
+ }
4263
+ return current;
4264
+ }
4265
+ function compare(a, b) {
4266
+ if (a === void 0 && b === void 0) return 0;
4267
+ if (a === void 0) return 1;
4268
+ if (b === void 0) return -1;
4269
+ if (typeof a === "number" && typeof b === "number") {
4270
+ return a - b;
4271
+ }
4272
+ return String(a).localeCompare(String(b));
4273
+ }
4274
+ function sortResults(items, sortSpec) {
4275
+ if (!sortSpec || items.length === 0) {
4276
+ return items;
4277
+ }
4278
+ const descending = sortSpec.startsWith("-");
4279
+ const field = descending ? sortSpec.slice(1) : sortSpec;
4280
+ const hasField = items.some((item) => getNestedValue(item, field) !== void 0);
4281
+ if (!hasField) {
4282
+ return items;
4283
+ }
4284
+ const sorted = [...items].sort((a, b) => {
4285
+ const aVal = getNestedValue(a, field);
4286
+ const bVal = getNestedValue(b, field);
4287
+ const result = compare(aVal, bVal);
4288
+ return descending ? -result : result;
4289
+ });
4290
+ return sorted;
4291
+ }
4292
+
4293
+ // src/commands/plugin-scaffold.ts
4294
+ import { mkdir as mkdir5, writeFile as writeFile6 } from "fs/promises";
4295
+ import { join as join7 } from "path";
4296
+ async function scaffoldPlugin(options) {
4297
+ const { name, dir, description = `GPC plugin: ${name}` } = options;
4298
+ const pluginName = name.startsWith("gpc-plugin-") ? name : `gpc-plugin-${name}`;
4299
+ const shortName = pluginName.replace(/^gpc-plugin-/, "");
4300
+ const srcDir = join7(dir, "src");
4301
+ const testDir = join7(dir, "tests");
4302
+ await mkdir5(srcDir, { recursive: true });
4303
+ await mkdir5(testDir, { recursive: true });
4304
+ const files = [];
4305
+ const pkg = {
4306
+ name: pluginName,
4307
+ version: "0.1.0",
4308
+ description,
4309
+ type: "module",
4310
+ main: "./dist/index.js",
4311
+ types: "./dist/index.d.ts",
4312
+ exports: {
4313
+ ".": {
4314
+ import: "./dist/index.js",
4315
+ types: "./dist/index.d.ts"
4316
+ }
4317
+ },
4318
+ files: ["dist"],
4319
+ scripts: {
4320
+ build: "tsup src/index.ts --format esm --dts",
4321
+ dev: "tsup src/index.ts --format esm --dts --watch",
4322
+ test: "vitest run",
4323
+ "test:watch": "vitest"
4324
+ },
4325
+ keywords: ["gpc", "gpc-plugin", "google-play"],
4326
+ license: "MIT",
4327
+ peerDependencies: {
4328
+ "@gpc-cli/plugin-sdk": ">=0.8.0"
4329
+ },
4330
+ devDependencies: {
4331
+ "@gpc-cli/plugin-sdk": "^0.8.0",
4332
+ tsup: "^8.0.0",
4333
+ typescript: "^5.0.0",
4334
+ vitest: "^3.0.0"
4335
+ }
4336
+ };
4337
+ await writeFile6(join7(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
4338
+ files.push("package.json");
4339
+ const tsconfig = {
4340
+ compilerOptions: {
4341
+ target: "ES2022",
4342
+ module: "ESNext",
4343
+ moduleResolution: "bundler",
4344
+ declaration: true,
4345
+ strict: true,
4346
+ esModuleInterop: true,
4347
+ skipLibCheck: true,
4348
+ outDir: "./dist",
4349
+ rootDir: "./src"
4350
+ },
4351
+ include: ["src"]
4352
+ };
4353
+ await writeFile6(join7(dir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2) + "\n");
4354
+ files.push("tsconfig.json");
4355
+ const srcContent = `import { definePlugin } from "@gpc-cli/plugin-sdk";
4356
+ import type { CommandEvent, CommandResult } from "@gpc-cli/plugin-sdk";
4357
+
4358
+ export const plugin = definePlugin({
4359
+ name: "${pluginName}",
4360
+ version: "0.1.0",
4361
+
4362
+ register(hooks) {
4363
+ hooks.beforeCommand(async (event: CommandEvent) => {
4364
+ // Called before every gpc command
4365
+ // Example: log command usage, validate prerequisites, etc.
4366
+ });
4367
+
4368
+ hooks.afterCommand(async (event: CommandEvent, result: CommandResult) => {
4369
+ // Called after every successful gpc command
4370
+ // Example: send notifications, update dashboards, etc.
4371
+ });
4372
+
4373
+ // Uncomment to add custom commands:
4374
+ // hooks.registerCommands((registry) => {
4375
+ // registry.add({
4376
+ // name: "${shortName}",
4377
+ // description: "${description}",
4378
+ // action: async (args, opts) => {
4379
+ // console.log("Hello from ${pluginName}!");
4380
+ // },
4381
+ // });
4382
+ // });
4383
+ },
4384
+ });
4385
+ `;
4386
+ await writeFile6(join7(srcDir, "index.ts"), srcContent);
4387
+ files.push("src/index.ts");
4388
+ const testContent = `import { describe, it, expect, vi } from "vitest";
4389
+ import { plugin } from "../src/index";
4390
+
4391
+ describe("${pluginName}", () => {
4392
+ it("has correct name and version", () => {
4393
+ expect(plugin.name).toBe("${pluginName}");
4394
+ expect(plugin.version).toBe("0.1.0");
4395
+ });
4396
+
4397
+ it("registers without errors", () => {
4398
+ const hooks = {
4399
+ beforeCommand: vi.fn(),
4400
+ afterCommand: vi.fn(),
4401
+ onError: vi.fn(),
4402
+ beforeRequest: vi.fn(),
4403
+ afterResponse: vi.fn(),
4404
+ registerCommands: vi.fn(),
4405
+ };
4406
+
4407
+ expect(() => plugin.register(hooks)).not.toThrow();
4408
+ });
4409
+ });
4410
+ `;
4411
+ await writeFile6(join7(testDir, "plugin.test.ts"), testContent);
4412
+ files.push("tests/plugin.test.ts");
4413
+ return { dir, files };
4414
+ }
4415
+
3594
4416
  // src/utils/webhooks.ts
3595
4417
  function formatSlackPayload(payload) {
3596
4418
  const status = payload.success ? "\u2713" : "\u2717";
@@ -3736,7 +4558,7 @@ function detectFileType(filePath) {
3736
4558
  }
3737
4559
 
3738
4560
  // src/commands/generated-apks.ts
3739
- import { writeFile as writeFile6 } from "fs/promises";
4561
+ import { writeFile as writeFile7 } from "fs/promises";
3740
4562
  async function listGeneratedApks(client, packageName, versionCode) {
3741
4563
  if (!Number.isInteger(versionCode) || versionCode <= 0) {
3742
4564
  throw new GpcError(
@@ -3767,7 +4589,7 @@ async function downloadGeneratedApk(client, packageName, versionCode, apkId, out
3767
4589
  }
3768
4590
  const buffer = await client.generatedApks.download(packageName, versionCode, apkId);
3769
4591
  const bytes = new Uint8Array(buffer);
3770
- await writeFile6(outputPath, bytes);
4592
+ await writeFile7(outputPath, bytes);
3771
4593
  return { path: outputPath, sizeBytes: bytes.byteLength };
3772
4594
  }
3773
4595
 
@@ -3834,7 +4656,7 @@ async function deactivatePurchaseOption(client, packageName, purchaseOptionId) {
3834
4656
  }
3835
4657
 
3836
4658
  // src/commands/bundle-analysis.ts
3837
- import { readFile as readFile9, stat as stat7 } from "fs/promises";
4659
+ import { readFile as readFile10, stat as stat7 } from "fs/promises";
3838
4660
  var EOCD_SIGNATURE = 101010256;
3839
4661
  var CD_SIGNATURE = 33639248;
3840
4662
  var MODULE_SUBDIRS = /* @__PURE__ */ new Set(["dex", "manifest", "res", "assets", "lib", "resources.pb", "root"]);
@@ -3912,7 +4734,7 @@ async function analyzeBundle(filePath) {
3912
4734
  if (!fileInfo || !fileInfo.isFile()) {
3913
4735
  throw new Error(`File not found: ${filePath}`);
3914
4736
  }
3915
- const buf = await readFile9(filePath);
4737
+ const buf = await readFile10(filePath);
3916
4738
  const cdEntries = parseCentralDirectory(buf);
3917
4739
  const fileType = detectFileType2(filePath);
3918
4740
  const isAab = fileType === "aab";
@@ -3984,19 +4806,52 @@ function compareBundles(before, after) {
3984
4806
  categoryDeltas
3985
4807
  };
3986
4808
  }
4809
+ function topFiles(analysis, n = 20) {
4810
+ return [...analysis.entries].sort((a, b) => b.compressedSize - a.compressedSize).slice(0, n);
4811
+ }
4812
+ async function checkBundleSize(analysis, configPath = ".bundlesize.json") {
4813
+ let config;
4814
+ try {
4815
+ const raw = await readFile10(configPath, "utf-8");
4816
+ config = JSON.parse(raw);
4817
+ } catch {
4818
+ return { passed: true, violations: [] };
4819
+ }
4820
+ const violations = [];
4821
+ if (config.maxTotalCompressed !== void 0 && analysis.totalCompressed > config.maxTotalCompressed) {
4822
+ violations.push({
4823
+ subject: "total",
4824
+ actual: analysis.totalCompressed,
4825
+ max: config.maxTotalCompressed
4826
+ });
4827
+ }
4828
+ if (config.modules) {
4829
+ for (const [moduleName, threshold] of Object.entries(config.modules)) {
4830
+ const mod = analysis.modules.find((m) => m.name === moduleName);
4831
+ if (mod && mod.compressedSize > threshold.maxCompressed) {
4832
+ violations.push({
4833
+ subject: `module:${moduleName}`,
4834
+ actual: mod.compressedSize,
4835
+ max: threshold.maxCompressed
4836
+ });
4837
+ }
4838
+ }
4839
+ }
4840
+ return { passed: violations.length === 0, violations };
4841
+ }
3987
4842
 
3988
4843
  // src/commands/status.ts
3989
- import { mkdir as mkdir5, readFile as readFile10, writeFile as writeFile7 } from "fs/promises";
4844
+ import { mkdir as mkdir6, readFile as readFile11, writeFile as writeFile8 } from "fs/promises";
3990
4845
  import { execSync } from "child_process";
3991
- import { join as join7 } from "path";
3992
- import { getCacheDir } from "@gpc-cli/config";
4846
+ import { join as join8 } from "path";
4847
+ import { getCacheDir as getCacheDir2 } from "@gpc-cli/config";
3993
4848
  var DEFAULT_TTL_SECONDS = 3600;
3994
4849
  function cacheFilePath(packageName) {
3995
- return join7(getCacheDir(), `status-${packageName}.json`);
4850
+ return join8(getCacheDir2(), `status-${packageName}.json`);
3996
4851
  }
3997
4852
  async function loadStatusCache(packageName, ttlSeconds = DEFAULT_TTL_SECONDS) {
3998
4853
  try {
3999
- const raw = await readFile10(cacheFilePath(packageName), "utf-8");
4854
+ const raw = await readFile11(cacheFilePath(packageName), "utf-8");
4000
4855
  const entry = JSON.parse(raw);
4001
4856
  const age = (Date.now() - new Date(entry.fetchedAt).getTime()) / 1e3;
4002
4857
  if (age > (entry.ttl ?? ttlSeconds)) return null;
@@ -4012,10 +4867,10 @@ async function loadStatusCache(packageName, ttlSeconds = DEFAULT_TTL_SECONDS) {
4012
4867
  }
4013
4868
  async function saveStatusCache(packageName, data, ttlSeconds = DEFAULT_TTL_SECONDS) {
4014
4869
  try {
4015
- const dir = getCacheDir();
4016
- await mkdir5(dir, { recursive: true });
4870
+ const dir = getCacheDir2();
4871
+ await mkdir6(dir, { recursive: true });
4017
4872
  const entry = { fetchedAt: data.fetchedAt, ttl: ttlSeconds, data };
4018
- await writeFile7(cacheFilePath(packageName), JSON.stringify(entry, null, 2), {
4873
+ await writeFile8(cacheFilePath(packageName), JSON.stringify(entry, null, 2), {
4019
4874
  encoding: "utf-8",
4020
4875
  mode: 384
4021
4876
  });
@@ -4380,20 +5235,20 @@ async function runWatchLoop(opts) {
4380
5235
  }
4381
5236
  }
4382
5237
  function breachStateFilePath(packageName) {
4383
- return join7(getCacheDir(), `breach-state-${packageName}.json`);
5238
+ return join8(getCacheDir2(), `breach-state-${packageName}.json`);
4384
5239
  }
4385
5240
  async function trackBreachState(packageName, isBreaching) {
4386
5241
  const filePath = breachStateFilePath(packageName);
4387
5242
  let prevBreaching = false;
4388
5243
  try {
4389
- const raw = await readFile10(filePath, "utf-8");
5244
+ const raw = await readFile11(filePath, "utf-8");
4390
5245
  prevBreaching = JSON.parse(raw).breaching;
4391
5246
  } catch {
4392
5247
  }
4393
5248
  if (prevBreaching !== isBreaching) {
4394
5249
  try {
4395
- await mkdir5(getCacheDir(), { recursive: true });
4396
- await writeFile7(
5250
+ await mkdir6(getCacheDir2(), { recursive: true });
5251
+ await writeFile8(
4397
5252
  filePath,
4398
5253
  JSON.stringify({ breaching: isBreaching, since: (/* @__PURE__ */ new Date()).toISOString() }, null, 2),
4399
5254
  { encoding: "utf-8", mode: 384 }
@@ -4433,6 +5288,7 @@ function statusHasBreach(status) {
4433
5288
  export {
4434
5289
  ApiError,
4435
5290
  ConfigError,
5291
+ DEFAULT_LIMITS,
4436
5292
  GOOGLE_PLAY_LANGUAGES,
4437
5293
  GpcError,
4438
5294
  NetworkError,
@@ -4440,26 +5296,34 @@ export {
4440
5296
  PluginManager,
4441
5297
  SENSITIVE_ARG_KEYS,
4442
5298
  SENSITIVE_KEYS,
5299
+ abortTrain,
4443
5300
  acknowledgeProductPurchase,
4444
5301
  activateBasePlan,
4445
5302
  activateOffer,
4446
5303
  activatePurchaseOption,
4447
5304
  addRecoveryTargeting,
4448
5305
  addTesters,
5306
+ advanceTrain,
4449
5307
  analyzeBundle,
5308
+ analyzeRemoteListings,
5309
+ analyzeReviews2 as analyzeReviews,
4450
5310
  batchSyncInAppProducts,
4451
5311
  cancelRecoveryAction,
4452
5312
  cancelSubscriptionPurchase,
5313
+ checkBundleSize,
4453
5314
  checkThreshold,
4454
5315
  clearAuditLog,
4455
5316
  compareBundles,
5317
+ compareVersionVitals,
4456
5318
  compareVitalsTrend,
4457
5319
  computeStatusDiff,
4458
5320
  consumeProductPurchase,
4459
5321
  convertRegionPrices,
4460
5322
  createAuditEntry,
4461
5323
  createDeviceTier,
5324
+ createEnterpriseApp,
4462
5325
  createExternalTransaction,
5326
+ createGrant,
4463
5327
  createInAppProduct,
4464
5328
  createOffer,
4465
5329
  createOneTimeOffer,
@@ -4474,6 +5338,7 @@ export {
4474
5338
  deactivatePurchaseOption,
4475
5339
  deferSubscriptionPurchase,
4476
5340
  deleteBasePlan,
5341
+ deleteGrant,
4477
5342
  deleteImage,
4478
5343
  deleteInAppProduct,
4479
5344
  deleteListing,
@@ -4486,6 +5351,7 @@ export {
4486
5351
  detectOutputFormat,
4487
5352
  diffListings,
4488
5353
  diffListingsCommand,
5354
+ diffListingsEnhanced,
4489
5355
  diffOneTimeProduct,
4490
5356
  diffReleases,
4491
5357
  diffSubscription,
@@ -4503,6 +5369,7 @@ export {
4503
5369
  formatStatusDiff,
4504
5370
  formatStatusSummary,
4505
5371
  formatStatusTable,
5372
+ formatWordDiff,
4506
5373
  generateMigrationPlan,
4507
5374
  generateNotesFromGit,
4508
5375
  getAppInfo,
@@ -4518,15 +5385,19 @@ export {
4518
5385
  getOneTimeProduct,
4519
5386
  getProductPurchase,
4520
5387
  getPurchaseOption,
5388
+ getQuotaUsage,
4521
5389
  getReleasesStatus,
4522
5390
  getReview,
4523
5391
  getSubscription,
5392
+ getSubscriptionAnalytics,
4524
5393
  getSubscriptionPurchase,
5394
+ getTrainStatus,
4525
5395
  getUser,
4526
5396
  getVitalsAnomalies,
4527
5397
  getVitalsAnr,
4528
5398
  getVitalsBattery,
4529
5399
  getVitalsCrashes,
5400
+ getVitalsLmk,
4530
5401
  getVitalsMemory,
4531
5402
  getVitalsOverview,
4532
5403
  getVitalsRendering,
@@ -4540,11 +5411,19 @@ export {
4540
5411
  isValidBcp47,
4541
5412
  isValidReportType,
4542
5413
  isValidStatsDimension,
5414
+ lintListing,
5415
+ lintListings,
5416
+ lintLocalListings,
5417
+ listAchievements,
4543
5418
  listAuditEvents,
4544
5419
  listDeviceTiers,
5420
+ listEnterpriseApps,
5421
+ listEvents,
4545
5422
  listGeneratedApks,
5423
+ listGrants,
4546
5424
  listImages,
4547
5425
  listInAppProducts,
5426
+ listLeaderboards,
4548
5427
  listOffers,
4549
5428
  listOneTimeOffers,
4550
5429
  listOneTimeProducts,
@@ -4558,11 +5437,13 @@ export {
4558
5437
  listUsers,
4559
5438
  listVoidedPurchases,
4560
5439
  loadStatusCache,
5440
+ maybePaginate,
4561
5441
  migratePrices,
4562
5442
  parseAppfile,
4563
5443
  parseFastfile,
4564
5444
  parseGrantArg,
4565
5445
  parseMonth,
5446
+ pauseTrain,
4566
5447
  promoteRelease,
4567
5448
  publish,
4568
5449
  pullListings,
@@ -4573,6 +5454,7 @@ export {
4573
5454
  redactSensitive,
4574
5455
  refundExternalTransaction,
4575
5456
  refundOrder,
5457
+ refundSubscriptionV2,
4576
5458
  removeTesters,
4577
5459
  removeUser,
4578
5460
  replyToReview,
@@ -4587,11 +5469,14 @@ export {
4587
5469
  sendNotification,
4588
5470
  sendWebhook,
4589
5471
  sortResults,
5472
+ startTrain,
4590
5473
  statusHasBreach,
4591
5474
  syncInAppProducts,
5475
+ topFiles,
4592
5476
  trackBreachState,
4593
5477
  updateAppDetails,
4594
5478
  updateDataSafety,
5479
+ updateGrant,
4595
5480
  updateInAppProduct,
4596
5481
  updateListing,
4597
5482
  updateOffer,
@@ -4614,6 +5499,8 @@ export {
4614
5499
  validateTrackName,
4615
5500
  validateUploadFile,
4616
5501
  validateVersionCode,
5502
+ watchVitalsWithAutoHalt,
5503
+ wordDiff,
4617
5504
  writeAuditLog,
4618
5505
  writeListingsToDir,
4619
5506
  writeMigrationOutput