@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/README.md +1 -1
- package/dist/index.d.ts +239 -2
- package/dist/index.js +1408 -471
- package/dist/index.js.map +1 -1
- package/package.json +7 -3
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
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
|
|
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(
|
|
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) => "
|
|
178
|
-
const body = rows.map(
|
|
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)
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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:
|
|
1362
|
-
const { join:
|
|
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 =
|
|
1394
|
-
await
|
|
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 =
|
|
1398
|
-
await
|
|
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(
|
|
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
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
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(
|
|
2670
|
-
return VALID_DIMENSIONS.has(
|
|
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)
|
|
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(
|
|
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/
|
|
3198
|
-
|
|
3199
|
-
var
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
const
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
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
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
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
|
|
3330
|
-
import { join as
|
|
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 =
|
|
3336
|
-
const testDir =
|
|
3337
|
-
await
|
|
3338
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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))
|
|
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))
|
|
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
|
|
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) ?? {
|
|
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) ?? {
|
|
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
|
|
3990
|
-
import {
|
|
3991
|
-
import { join as
|
|
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
|
|
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
|
|
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 =
|
|
4016
|
-
await
|
|
4908
|
+
const dir = getCacheDir2();
|
|
4909
|
+
await mkdir6(dir, { recursive: true });
|
|
4017
4910
|
const entry = { fetchedAt: data.fetchedAt, ttl: ttlSeconds, data };
|
|
4018
|
-
await
|
|
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 [
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
4396
|
-
await
|
|
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
|
-
|
|
4413
|
-
|
|
4414
|
-
{
|
|
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
|
-
|
|
4418
|
-
stdio: "ignore"
|
|
4419
|
-
});
|
|
5322
|
+
execFile2("notify-send", [title, body]);
|
|
4420
5323
|
} else if (p === "win32") {
|
|
4421
|
-
const
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
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
|