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