@gpc-cli/core 0.9.20 → 0.9.22
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 +11 -5
- package/dist/index.d.ts +105 -1
- package/dist/index.js +477 -51
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -244,7 +244,7 @@ function buildTestCase(item, commandName, index = 0) {
|
|
|
244
244
|
const record = item;
|
|
245
245
|
const name = escapeXml(
|
|
246
246
|
String(
|
|
247
|
-
record["name"] ?? record["title"] ?? record["sku"] ?? record["id"] ?? record["productId"] ?? record["packageName"] ?? record["trackId"] ?? record["region"] ?? record["languageCode"] ?? `item-${index + 1}`
|
|
247
|
+
record["name"] ?? record["title"] ?? record["sku"] ?? record["id"] ?? record["reviewId"] ?? record["productId"] ?? record["packageName"] ?? record["track"] ?? record["trackId"] ?? record["versionCode"] ?? record["region"] ?? record["languageCode"] ?? `item-${index + 1}`
|
|
248
248
|
)
|
|
249
249
|
);
|
|
250
250
|
const classname = `gpc.${escapeXml(commandName)}`;
|
|
@@ -1329,8 +1329,8 @@ var ALL_IMAGE_TYPES = [
|
|
|
1329
1329
|
"tvBanner"
|
|
1330
1330
|
];
|
|
1331
1331
|
async function exportImages(client, packageName, dir, options) {
|
|
1332
|
-
const { mkdir:
|
|
1333
|
-
const { join:
|
|
1332
|
+
const { mkdir: mkdir6, writeFile: writeFile8 } = await import("fs/promises");
|
|
1333
|
+
const { join: join8 } = await import("path");
|
|
1334
1334
|
const edit = await client.edits.insert(packageName);
|
|
1335
1335
|
try {
|
|
1336
1336
|
let languages;
|
|
@@ -1361,12 +1361,12 @@ async function exportImages(client, packageName, dir, options) {
|
|
|
1361
1361
|
const batch = tasks.slice(i, i + concurrency);
|
|
1362
1362
|
const results = await Promise.all(
|
|
1363
1363
|
batch.map(async (task) => {
|
|
1364
|
-
const dirPath =
|
|
1365
|
-
await
|
|
1364
|
+
const dirPath = join8(dir, task.language, task.imageType);
|
|
1365
|
+
await mkdir6(dirPath, { recursive: true });
|
|
1366
1366
|
const response = await fetch(task.url);
|
|
1367
1367
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
1368
|
-
const filePath =
|
|
1369
|
-
await
|
|
1368
|
+
const filePath = join8(dirPath, `${task.index}.png`);
|
|
1369
|
+
await writeFile8(filePath, buffer);
|
|
1370
1370
|
return buffer.length;
|
|
1371
1371
|
})
|
|
1372
1372
|
);
|
|
@@ -1404,6 +1404,7 @@ async function updateAppDetails(client, packageName, details) {
|
|
|
1404
1404
|
// src/commands/migrate.ts
|
|
1405
1405
|
import { readdir as readdir2, readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2, access } from "fs/promises";
|
|
1406
1406
|
import { join as join2 } from "path";
|
|
1407
|
+
var COMPLEX_RUBY_RE = /\b(begin|rescue|ensure|if |unless |case |while |until |for )\b/;
|
|
1407
1408
|
async function fileExists(path) {
|
|
1408
1409
|
try {
|
|
1409
1410
|
await access(path);
|
|
@@ -1419,7 +1420,8 @@ async function detectFastlane(cwd) {
|
|
|
1419
1420
|
hasMetadata: false,
|
|
1420
1421
|
hasGemfile: false,
|
|
1421
1422
|
lanes: [],
|
|
1422
|
-
metadataLanguages: []
|
|
1423
|
+
metadataLanguages: [],
|
|
1424
|
+
parseWarnings: []
|
|
1423
1425
|
};
|
|
1424
1426
|
const fastlaneDir = join2(cwd, "fastlane");
|
|
1425
1427
|
const hasFastlaneDir = await fileExists(fastlaneDir);
|
|
@@ -1435,13 +1437,20 @@ async function detectFastlane(cwd) {
|
|
|
1435
1437
|
const entries = await readdir2(metadataDir, { withFileTypes: true });
|
|
1436
1438
|
result.metadataLanguages = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1437
1439
|
} catch {
|
|
1440
|
+
result.parseWarnings.push("Could not read metadata directory \u2014 check permissions");
|
|
1438
1441
|
}
|
|
1439
1442
|
}
|
|
1440
1443
|
if (result.hasFastfile) {
|
|
1441
1444
|
try {
|
|
1442
1445
|
const content = await readFile3(fastfilePath, "utf-8");
|
|
1443
1446
|
result.lanes = parseFastfile(content);
|
|
1447
|
+
if (COMPLEX_RUBY_RE.test(content)) {
|
|
1448
|
+
result.parseWarnings.push(
|
|
1449
|
+
"Fastfile contains complex Ruby constructs (begin/rescue/if/unless/case). Lane detection may be incomplete \u2014 review MIGRATION.md and adjust manually."
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1444
1452
|
} catch {
|
|
1453
|
+
result.parseWarnings.push("Could not read Fastfile \u2014 check permissions");
|
|
1445
1454
|
}
|
|
1446
1455
|
}
|
|
1447
1456
|
if (result.hasAppfile) {
|
|
@@ -1451,6 +1460,7 @@ async function detectFastlane(cwd) {
|
|
|
1451
1460
|
result.packageName = parsed.packageName;
|
|
1452
1461
|
result.jsonKeyPath = parsed.jsonKeyPath;
|
|
1453
1462
|
} catch {
|
|
1463
|
+
result.parseWarnings.push("Could not read Appfile \u2014 check permissions");
|
|
1454
1464
|
}
|
|
1455
1465
|
}
|
|
1456
1466
|
return result;
|
|
@@ -1481,8 +1491,9 @@ function mapLaneToGpc(name, actions, body) {
|
|
|
1481
1491
|
const trackMatch = body.match(/track\s*:\s*["'](\w+)["']/);
|
|
1482
1492
|
const rolloutMatch = body.match(/rollout\s*:\s*["']?([\d.]+)["']?/);
|
|
1483
1493
|
if (rolloutMatch) {
|
|
1484
|
-
const
|
|
1485
|
-
|
|
1494
|
+
const raw = parseFloat(rolloutMatch[1] ?? "0");
|
|
1495
|
+
const pct = raw > 1 ? Math.round(raw) : Math.round(raw * 100);
|
|
1496
|
+
return `gpc releases upload --rollout ${pct}${trackMatch ? ` --track ${trackMatch[1]}` : ""}`;
|
|
1486
1497
|
}
|
|
1487
1498
|
if (trackMatch) {
|
|
1488
1499
|
return `gpc releases upload --track ${trackMatch[1]}`;
|
|
@@ -1512,7 +1523,7 @@ function parseAppfile(content) {
|
|
|
1512
1523
|
function generateMigrationPlan(detection) {
|
|
1513
1524
|
const config = {};
|
|
1514
1525
|
const checklist = [];
|
|
1515
|
-
const warnings = [];
|
|
1526
|
+
const warnings = [...detection.parseWarnings];
|
|
1516
1527
|
if (detection.packageName) {
|
|
1517
1528
|
config["app"] = detection.packageName;
|
|
1518
1529
|
} else {
|
|
@@ -1521,44 +1532,61 @@ function generateMigrationPlan(detection) {
|
|
|
1521
1532
|
if (detection.jsonKeyPath) {
|
|
1522
1533
|
config["auth"] = { serviceAccount: detection.jsonKeyPath };
|
|
1523
1534
|
} else {
|
|
1524
|
-
checklist.push("Configure authentication: gpc auth
|
|
1535
|
+
checklist.push("Configure authentication: gpc auth login");
|
|
1525
1536
|
}
|
|
1526
1537
|
for (const lane of detection.lanes) {
|
|
1527
1538
|
if (lane.gpcEquivalent) {
|
|
1528
|
-
checklist.push(
|
|
1539
|
+
checklist.push(
|
|
1540
|
+
`Replace Fastlane lane "${lane.name}" with: ${lane.gpcEquivalent} <your.aab>`
|
|
1541
|
+
);
|
|
1529
1542
|
}
|
|
1530
1543
|
if (lane.actions.includes("capture_android_screenshots")) {
|
|
1531
1544
|
warnings.push(
|
|
1532
|
-
`Lane "${lane.name}" uses capture_android_screenshots which has no GPC equivalent.
|
|
1545
|
+
`Lane "${lane.name}" uses capture_android_screenshots which has no GPC equivalent. Use a separate screenshot tool or check gpc plugins list for community plugins.`
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1548
|
+
if (lane.actions.length === 0 || lane.gpcEquivalent === void 0 && !lane.actions.includes("capture_android_screenshots")) {
|
|
1549
|
+
warnings.push(
|
|
1550
|
+
`Lane "${lane.name}" has no automatic GPC equivalent. Check \`gpc plugins list\` or the plugin SDK docs to build a custom command.`
|
|
1533
1551
|
);
|
|
1534
1552
|
}
|
|
1535
1553
|
}
|
|
1536
1554
|
if (detection.hasMetadata && detection.metadataLanguages.length > 0) {
|
|
1555
|
+
const langs = detection.metadataLanguages.slice(0, 3).join(", ");
|
|
1556
|
+
const more = detection.metadataLanguages.length > 3 ? ` (+${detection.metadataLanguages.length - 3} more)` : "";
|
|
1557
|
+
checklist.push(
|
|
1558
|
+
`Pull current metadata for ${detection.metadataLanguages.length} language(s) (${langs}${more}): gpc listings pull --dir fastlane/metadata/android`
|
|
1559
|
+
);
|
|
1537
1560
|
checklist.push(
|
|
1538
|
-
|
|
1561
|
+
"Review pulled metadata, then push back: gpc listings push --dir fastlane/metadata/android"
|
|
1539
1562
|
);
|
|
1540
|
-
checklist.push("Review and push metadata: gpc listings push --dir metadata");
|
|
1541
1563
|
}
|
|
1542
1564
|
checklist.push("Run gpc doctor to verify your setup");
|
|
1543
|
-
checklist.push("Test with --dry-run before making real changes");
|
|
1565
|
+
checklist.push("Test each command with --dry-run before making real changes");
|
|
1544
1566
|
if (detection.hasGemfile) {
|
|
1545
1567
|
checklist.push("Remove Fastlane from your Gemfile once migration is complete");
|
|
1546
1568
|
}
|
|
1547
|
-
if (detection.lanes.some(
|
|
1548
|
-
|
|
1569
|
+
if (detection.lanes.some(
|
|
1570
|
+
(l) => l.actions.includes("supply") || l.actions.includes("upload_to_play_store")
|
|
1571
|
+
)) {
|
|
1572
|
+
checklist.push("Update CI/CD pipelines to call gpc commands instead of Fastlane lanes");
|
|
1549
1573
|
}
|
|
1550
1574
|
return { config, checklist, warnings };
|
|
1551
1575
|
}
|
|
1552
1576
|
async function writeMigrationOutput(result, dir) {
|
|
1553
1577
|
await mkdir2(dir, { recursive: true });
|
|
1554
1578
|
const files = [];
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1579
|
+
if (Object.keys(result.config).length > 0) {
|
|
1580
|
+
const configPath = join2(dir, ".gpcrc.json");
|
|
1581
|
+
await writeFile2(configPath, JSON.stringify(result.config, null, 2) + "\n", "utf-8");
|
|
1582
|
+
files.push(configPath);
|
|
1583
|
+
}
|
|
1558
1584
|
const migrationPath = join2(dir, "MIGRATION.md");
|
|
1559
1585
|
const lines = [
|
|
1560
1586
|
"# Fastlane to GPC Migration",
|
|
1561
1587
|
"",
|
|
1588
|
+
"Generated by `gpc migrate fastlane`. Review and adjust before applying.",
|
|
1589
|
+
"",
|
|
1562
1590
|
"## Migration Checklist",
|
|
1563
1591
|
""
|
|
1564
1592
|
];
|
|
@@ -1570,7 +1598,7 @@ async function writeMigrationOutput(result, dir) {
|
|
|
1570
1598
|
lines.push("## Warnings");
|
|
1571
1599
|
lines.push("");
|
|
1572
1600
|
for (const warning of result.warnings) {
|
|
1573
|
-
lines.push(
|
|
1601
|
+
lines.push(`> \u26A0 ${warning}`);
|
|
1574
1602
|
}
|
|
1575
1603
|
}
|
|
1576
1604
|
lines.push("");
|
|
@@ -1581,8 +1609,11 @@ async function writeMigrationOutput(result, dir) {
|
|
|
1581
1609
|
lines.push("| `fastlane supply` | `gpc releases upload` / `gpc listings push` |");
|
|
1582
1610
|
lines.push("| `upload_to_play_store` | `gpc releases upload` |");
|
|
1583
1611
|
lines.push('| `supply(track: "internal")` | `gpc releases upload --track internal` |');
|
|
1584
|
-
lines.push('| `supply(rollout: "0.1")` | `gpc releases
|
|
1585
|
-
lines.push("| `
|
|
1612
|
+
lines.push('| `supply(rollout: "0.1")` | `gpc releases upload --rollout 10` |');
|
|
1613
|
+
lines.push("| `supply(skip_upload_aab: true)` | `gpc listings push` |");
|
|
1614
|
+
lines.push("| `capture_android_screenshots` | No equivalent \u2014 use separate tool |");
|
|
1615
|
+
lines.push("");
|
|
1616
|
+
lines.push("See the full migration guide: https://yasserstudio.github.io/gpc/migration/from-fastlane");
|
|
1586
1617
|
lines.push("");
|
|
1587
1618
|
await writeFile2(migrationPath, lines.join("\n"), "utf-8");
|
|
1588
1619
|
files.push(migrationPath);
|
|
@@ -1729,12 +1760,16 @@ var STANDARD_TRACKS = /* @__PURE__ */ new Set([
|
|
|
1729
1760
|
var TRACK_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_:-]*$/;
|
|
1730
1761
|
async function validatePreSubmission(options) {
|
|
1731
1762
|
const checks = [];
|
|
1763
|
+
const resultWarnings = [];
|
|
1732
1764
|
const fileResult = await validateUploadFile(options.filePath);
|
|
1733
1765
|
checks.push({
|
|
1734
1766
|
name: "file",
|
|
1735
1767
|
passed: fileResult.valid,
|
|
1736
|
-
message: fileResult.valid ? `Valid ${fileResult.fileType}
|
|
1768
|
+
message: fileResult.valid ? `Valid ${fileResult.fileType.toUpperCase()} (${formatSize3(fileResult.sizeBytes)})` : fileResult.errors.join("; ")
|
|
1737
1769
|
});
|
|
1770
|
+
for (const w of fileResult.warnings) {
|
|
1771
|
+
resultWarnings.push(w);
|
|
1772
|
+
}
|
|
1738
1773
|
if (options.mappingFile) {
|
|
1739
1774
|
try {
|
|
1740
1775
|
const stats = await stat5(options.mappingFile);
|
|
@@ -1786,7 +1821,8 @@ async function validatePreSubmission(options) {
|
|
|
1786
1821
|
}
|
|
1787
1822
|
return {
|
|
1788
1823
|
valid: checks.every((c) => c.passed),
|
|
1789
|
-
checks
|
|
1824
|
+
checks,
|
|
1825
|
+
warnings: resultWarnings
|
|
1790
1826
|
};
|
|
1791
1827
|
}
|
|
1792
1828
|
function formatSize3(bytes) {
|
|
@@ -1955,23 +1991,22 @@ var METRIC_SET_METRICS = {
|
|
|
1955
1991
|
function buildQuery(metricSet, options) {
|
|
1956
1992
|
const metrics = METRIC_SET_METRICS[metricSet] ?? ["errorReportCount", "distinctUsers"];
|
|
1957
1993
|
const days = options?.days ?? 30;
|
|
1958
|
-
const
|
|
1959
|
-
end
|
|
1960
|
-
const start = new Date(
|
|
1961
|
-
start.setDate(start.getDate() - days);
|
|
1994
|
+
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
1995
|
+
const end = new Date(Date.now() - DAY_MS);
|
|
1996
|
+
const start = new Date(Date.now() - DAY_MS - days * DAY_MS);
|
|
1962
1997
|
const query = {
|
|
1963
1998
|
metrics,
|
|
1964
1999
|
timelineSpec: {
|
|
1965
2000
|
aggregationPeriod: options?.aggregation ?? "DAILY",
|
|
1966
2001
|
startTime: {
|
|
1967
|
-
year: start.
|
|
1968
|
-
month: start.
|
|
1969
|
-
day: start.
|
|
2002
|
+
year: start.getUTCFullYear(),
|
|
2003
|
+
month: start.getUTCMonth() + 1,
|
|
2004
|
+
day: start.getUTCDate()
|
|
1970
2005
|
},
|
|
1971
2006
|
endTime: {
|
|
1972
|
-
year: end.
|
|
1973
|
-
month: end.
|
|
1974
|
-
day: end.
|
|
2007
|
+
year: end.getUTCFullYear(),
|
|
2008
|
+
month: end.getUTCMonth() + 1,
|
|
2009
|
+
day: end.getUTCDate()
|
|
1975
2010
|
}
|
|
1976
2011
|
}
|
|
1977
2012
|
};
|
|
@@ -2036,21 +2071,25 @@ async function searchVitalsErrors(reporting, packageName, options) {
|
|
|
2036
2071
|
return reporting.searchErrorIssues(packageName, options?.filter, options?.maxResults);
|
|
2037
2072
|
}
|
|
2038
2073
|
async function compareVitalsTrend(reporting, packageName, metricSet, days = 7) {
|
|
2039
|
-
const
|
|
2040
|
-
|
|
2041
|
-
const
|
|
2042
|
-
const
|
|
2043
|
-
currentStart
|
|
2044
|
-
const previousEnd = new Date(
|
|
2045
|
-
const previousStart = new Date(
|
|
2046
|
-
previousStart.setDate(previousStart.getDate() - days);
|
|
2074
|
+
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2075
|
+
const nowMs = Date.now();
|
|
2076
|
+
const baseMs = nowMs - 2 * DAY_MS;
|
|
2077
|
+
const currentEnd = new Date(baseMs);
|
|
2078
|
+
const currentStart = new Date(baseMs - days * DAY_MS);
|
|
2079
|
+
const previousEnd = new Date(baseMs - days * DAY_MS - DAY_MS);
|
|
2080
|
+
const previousStart = new Date(baseMs - days * DAY_MS - DAY_MS - days * DAY_MS);
|
|
2047
2081
|
const metrics = METRIC_SET_METRICS[metricSet] ?? ["errorReportCount", "distinctUsers"];
|
|
2082
|
+
const toApiDate2 = (d) => ({
|
|
2083
|
+
year: d.getUTCFullYear(),
|
|
2084
|
+
month: d.getUTCMonth() + 1,
|
|
2085
|
+
day: d.getUTCDate()
|
|
2086
|
+
});
|
|
2048
2087
|
const makeQuery = (start, end) => ({
|
|
2049
2088
|
metrics,
|
|
2050
2089
|
timelineSpec: {
|
|
2051
2090
|
aggregationPeriod: "DAILY",
|
|
2052
|
-
startTime:
|
|
2053
|
-
endTime:
|
|
2091
|
+
startTime: toApiDate2(start),
|
|
2092
|
+
endTime: toApiDate2(end)
|
|
2054
2093
|
}
|
|
2055
2094
|
});
|
|
2056
2095
|
const [currentResult, previousResult] = await Promise.all([
|
|
@@ -2797,10 +2836,12 @@ function formatNotes(commits, maxLength) {
|
|
|
2797
2836
|
${bullets}`);
|
|
2798
2837
|
}
|
|
2799
2838
|
let text = sections.join("\n\n");
|
|
2839
|
+
let truncated = false;
|
|
2800
2840
|
if (text.length > maxLength) {
|
|
2801
2841
|
text = text.slice(0, maxLength - 3) + "...";
|
|
2842
|
+
truncated = true;
|
|
2802
2843
|
}
|
|
2803
|
-
return text;
|
|
2844
|
+
return { text, truncated };
|
|
2804
2845
|
}
|
|
2805
2846
|
async function gitExec(args) {
|
|
2806
2847
|
try {
|
|
@@ -2854,17 +2895,19 @@ async function generateNotesFromGit(options) {
|
|
|
2854
2895
|
language,
|
|
2855
2896
|
text: "No changes since last release.",
|
|
2856
2897
|
commitCount: 0,
|
|
2857
|
-
since
|
|
2898
|
+
since,
|
|
2899
|
+
truncated: false
|
|
2858
2900
|
};
|
|
2859
2901
|
}
|
|
2860
2902
|
const subjects = logOutput.split("\n").filter((line) => line.length > 0);
|
|
2861
2903
|
const commits = subjects.map(parseConventionalCommit);
|
|
2862
|
-
const text = formatNotes(commits, maxLength);
|
|
2904
|
+
const { text, truncated } = formatNotes(commits, maxLength);
|
|
2863
2905
|
return {
|
|
2864
2906
|
language,
|
|
2865
2907
|
text,
|
|
2866
2908
|
commitCount: subjects.length,
|
|
2867
|
-
since
|
|
2909
|
+
since,
|
|
2910
|
+
truncated
|
|
2868
2911
|
};
|
|
2869
2912
|
}
|
|
2870
2913
|
|
|
@@ -3759,6 +3802,382 @@ async function deactivatePurchaseOption(client, packageName, purchaseOptionId) {
|
|
|
3759
3802
|
);
|
|
3760
3803
|
}
|
|
3761
3804
|
}
|
|
3805
|
+
|
|
3806
|
+
// src/commands/bundle-analysis.ts
|
|
3807
|
+
import { readFile as readFile9, stat as stat6 } from "fs/promises";
|
|
3808
|
+
var EOCD_SIGNATURE = 101010256;
|
|
3809
|
+
var CD_SIGNATURE = 33639248;
|
|
3810
|
+
var MODULE_SUBDIRS = /* @__PURE__ */ new Set(["dex", "manifest", "res", "assets", "lib", "resources.pb", "root"]);
|
|
3811
|
+
function detectCategory(path) {
|
|
3812
|
+
const lower = path.toLowerCase();
|
|
3813
|
+
if (lower.endsWith(".dex") || /\/dex\/[^/]+\.dex$/.test(lower)) return "dex";
|
|
3814
|
+
if (lower === "resources.arsc" || lower.endsWith("/resources.arsc") || lower.endsWith("/resources.pb") || /^(([^/]+\/)?res\/)/.test(lower)) return "resources";
|
|
3815
|
+
if (/^(([^/]+\/)?assets\/)/.test(lower)) return "assets";
|
|
3816
|
+
if (/^(([^/]+\/)?lib\/)/.test(lower)) return "native-libs";
|
|
3817
|
+
if (lower === "androidmanifest.xml" || lower.endsWith("/androidmanifest.xml") || /^(([^/]+\/)?manifest\/)/.test(lower)) return "manifest";
|
|
3818
|
+
if (lower.startsWith("meta-inf/") || lower === "meta-inf") return "signing";
|
|
3819
|
+
return "other";
|
|
3820
|
+
}
|
|
3821
|
+
function detectModule(path, isAab) {
|
|
3822
|
+
if (!isAab) return "(root)";
|
|
3823
|
+
const slashIdx = path.indexOf("/");
|
|
3824
|
+
if (slashIdx === -1) return "(root)";
|
|
3825
|
+
const topDir = path.substring(0, slashIdx);
|
|
3826
|
+
const rest = path.substring(slashIdx + 1);
|
|
3827
|
+
if (topDir === "base") return "base";
|
|
3828
|
+
if (topDir === "BUNDLE-METADATA" || topDir === "META-INF") return "(root)";
|
|
3829
|
+
if (path === "BundleConfig.pb") return "(root)";
|
|
3830
|
+
const subDir = rest.split("/")[0] || "";
|
|
3831
|
+
if (MODULE_SUBDIRS.has(subDir)) return topDir;
|
|
3832
|
+
return "(root)";
|
|
3833
|
+
}
|
|
3834
|
+
function parseCentralDirectory(buf) {
|
|
3835
|
+
let eocdOffset = -1;
|
|
3836
|
+
for (let i = buf.length - 22; i >= 0 && i >= buf.length - 65557; i--) {
|
|
3837
|
+
if (buf.readUInt32LE(i) === EOCD_SIGNATURE) {
|
|
3838
|
+
eocdOffset = i;
|
|
3839
|
+
break;
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
if (eocdOffset === -1) {
|
|
3843
|
+
throw new Error("Not a valid ZIP file: EOCD signature not found");
|
|
3844
|
+
}
|
|
3845
|
+
const cdSize = buf.readUInt32LE(eocdOffset + 12);
|
|
3846
|
+
let cdOffset = buf.readUInt32LE(eocdOffset + 16);
|
|
3847
|
+
if (cdOffset === 4294967295) {
|
|
3848
|
+
const zip64LocatorOffset = eocdOffset - 20;
|
|
3849
|
+
if (zip64LocatorOffset >= 0 && buf.readUInt32LE(zip64LocatorOffset) === 117853008) {
|
|
3850
|
+
const zip64EocdOffset = Number(buf.readBigUInt64LE(zip64LocatorOffset + 8));
|
|
3851
|
+
if (zip64EocdOffset >= 0 && zip64EocdOffset < buf.length) {
|
|
3852
|
+
cdOffset = Number(buf.readBigUInt64LE(zip64EocdOffset + 48));
|
|
3853
|
+
}
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3856
|
+
const entries = [];
|
|
3857
|
+
let pos = cdOffset;
|
|
3858
|
+
const end = cdOffset + cdSize;
|
|
3859
|
+
while (pos < end && pos + 46 <= buf.length) {
|
|
3860
|
+
const sig = buf.readUInt32LE(pos);
|
|
3861
|
+
if (sig !== CD_SIGNATURE) break;
|
|
3862
|
+
const compressedSize = buf.readUInt32LE(pos + 20);
|
|
3863
|
+
const uncompressedSize = buf.readUInt32LE(pos + 24);
|
|
3864
|
+
const filenameLen = buf.readUInt16LE(pos + 28);
|
|
3865
|
+
const extraLen = buf.readUInt16LE(pos + 30);
|
|
3866
|
+
const commentLen = buf.readUInt16LE(pos + 32);
|
|
3867
|
+
const filename = buf.toString("utf-8", pos + 46, pos + 46 + filenameLen);
|
|
3868
|
+
if (!filename.endsWith("/")) {
|
|
3869
|
+
entries.push({ filename, compressedSize, uncompressedSize });
|
|
3870
|
+
}
|
|
3871
|
+
pos += 46 + filenameLen + extraLen + commentLen;
|
|
3872
|
+
}
|
|
3873
|
+
return entries;
|
|
3874
|
+
}
|
|
3875
|
+
function detectFileType2(filePath) {
|
|
3876
|
+
const lower = filePath.toLowerCase();
|
|
3877
|
+
if (lower.endsWith(".aab")) return "aab";
|
|
3878
|
+
return "apk";
|
|
3879
|
+
}
|
|
3880
|
+
async function analyzeBundle(filePath) {
|
|
3881
|
+
const fileInfo = await stat6(filePath).catch(() => null);
|
|
3882
|
+
if (!fileInfo || !fileInfo.isFile()) {
|
|
3883
|
+
throw new Error(`File not found: ${filePath}`);
|
|
3884
|
+
}
|
|
3885
|
+
const buf = await readFile9(filePath);
|
|
3886
|
+
const cdEntries = parseCentralDirectory(buf);
|
|
3887
|
+
const fileType = detectFileType2(filePath);
|
|
3888
|
+
const isAab = fileType === "aab";
|
|
3889
|
+
const entries = cdEntries.map((e) => ({
|
|
3890
|
+
path: e.filename,
|
|
3891
|
+
module: detectModule(e.filename, isAab),
|
|
3892
|
+
category: detectCategory(e.filename),
|
|
3893
|
+
compressedSize: e.compressedSize,
|
|
3894
|
+
uncompressedSize: e.uncompressedSize
|
|
3895
|
+
}));
|
|
3896
|
+
const moduleMap = /* @__PURE__ */ new Map();
|
|
3897
|
+
for (const entry of entries) {
|
|
3898
|
+
const existing = moduleMap.get(entry.module) ?? { compressedSize: 0, uncompressedSize: 0, entries: 0 };
|
|
3899
|
+
existing.compressedSize += entry.compressedSize;
|
|
3900
|
+
existing.uncompressedSize += entry.uncompressedSize;
|
|
3901
|
+
existing.entries += 1;
|
|
3902
|
+
moduleMap.set(entry.module, existing);
|
|
3903
|
+
}
|
|
3904
|
+
const categoryMap = /* @__PURE__ */ new Map();
|
|
3905
|
+
for (const entry of entries) {
|
|
3906
|
+
const existing = categoryMap.get(entry.category) ?? { compressedSize: 0, uncompressedSize: 0, entries: 0 };
|
|
3907
|
+
existing.compressedSize += entry.compressedSize;
|
|
3908
|
+
existing.uncompressedSize += entry.uncompressedSize;
|
|
3909
|
+
existing.entries += 1;
|
|
3910
|
+
categoryMap.set(entry.category, existing);
|
|
3911
|
+
}
|
|
3912
|
+
const modules = [...moduleMap.entries()].map(([name, data]) => ({ name, ...data })).sort((a, b) => b.compressedSize - a.compressedSize);
|
|
3913
|
+
const categories = [...categoryMap.entries()].map(([name, data]) => ({ name, ...data })).sort((a, b) => b.compressedSize - a.compressedSize);
|
|
3914
|
+
const totalCompressed = entries.reduce((sum, e) => sum + e.compressedSize, 0);
|
|
3915
|
+
const totalUncompressed = entries.reduce((sum, e) => sum + e.uncompressedSize, 0);
|
|
3916
|
+
return {
|
|
3917
|
+
filePath,
|
|
3918
|
+
fileType,
|
|
3919
|
+
totalCompressed,
|
|
3920
|
+
totalUncompressed,
|
|
3921
|
+
entryCount: entries.length,
|
|
3922
|
+
modules,
|
|
3923
|
+
categories,
|
|
3924
|
+
entries
|
|
3925
|
+
};
|
|
3926
|
+
}
|
|
3927
|
+
function compareBundles(before, after) {
|
|
3928
|
+
const sizeDelta = after.totalCompressed - before.totalCompressed;
|
|
3929
|
+
const sizeDeltaPercent = before.totalCompressed > 0 ? Math.round(sizeDelta / before.totalCompressed * 100 * 10) / 10 : 0;
|
|
3930
|
+
const allModules = /* @__PURE__ */ new Set([
|
|
3931
|
+
...before.modules.map((m) => m.name),
|
|
3932
|
+
...after.modules.map((m) => m.name)
|
|
3933
|
+
]);
|
|
3934
|
+
const moduleDeltas = [...allModules].map((module) => {
|
|
3935
|
+
const b = before.modules.find((m) => m.name === module)?.compressedSize ?? 0;
|
|
3936
|
+
const a = after.modules.find((m) => m.name === module)?.compressedSize ?? 0;
|
|
3937
|
+
return { module, before: b, after: a, delta: a - b };
|
|
3938
|
+
}).sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta));
|
|
3939
|
+
const allCategories = /* @__PURE__ */ new Set([
|
|
3940
|
+
...before.categories.map((c) => c.name),
|
|
3941
|
+
...after.categories.map((c) => c.name)
|
|
3942
|
+
]);
|
|
3943
|
+
const categoryDeltas = [...allCategories].map((category) => {
|
|
3944
|
+
const b = before.categories.find((c) => c.name === category)?.compressedSize ?? 0;
|
|
3945
|
+
const a = after.categories.find((c) => c.name === category)?.compressedSize ?? 0;
|
|
3946
|
+
return { category, before: b, after: a, delta: a - b };
|
|
3947
|
+
}).sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta));
|
|
3948
|
+
return {
|
|
3949
|
+
before: { path: before.filePath, totalCompressed: before.totalCompressed },
|
|
3950
|
+
after: { path: after.filePath, totalCompressed: after.totalCompressed },
|
|
3951
|
+
sizeDelta,
|
|
3952
|
+
sizeDeltaPercent,
|
|
3953
|
+
moduleDeltas,
|
|
3954
|
+
categoryDeltas
|
|
3955
|
+
};
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3958
|
+
// src/commands/status.ts
|
|
3959
|
+
import { mkdir as mkdir5, readFile as readFile10, writeFile as writeFile7 } from "fs/promises";
|
|
3960
|
+
import { join as join7 } from "path";
|
|
3961
|
+
import { getCacheDir } from "@gpc-cli/config";
|
|
3962
|
+
var DEFAULT_TTL_SECONDS = 3600;
|
|
3963
|
+
function cacheFilePath(packageName) {
|
|
3964
|
+
return join7(getCacheDir(), `status-${packageName}.json`);
|
|
3965
|
+
}
|
|
3966
|
+
async function loadStatusCache(packageName, ttlSeconds = DEFAULT_TTL_SECONDS) {
|
|
3967
|
+
try {
|
|
3968
|
+
const raw = await readFile10(cacheFilePath(packageName), "utf-8");
|
|
3969
|
+
const entry = JSON.parse(raw);
|
|
3970
|
+
const age = (Date.now() - new Date(entry.fetchedAt).getTime()) / 1e3;
|
|
3971
|
+
if (age > (entry.ttl ?? ttlSeconds)) return null;
|
|
3972
|
+
return { ...entry.data, cached: true };
|
|
3973
|
+
} catch {
|
|
3974
|
+
return null;
|
|
3975
|
+
}
|
|
3976
|
+
}
|
|
3977
|
+
async function saveStatusCache(packageName, data, ttlSeconds = DEFAULT_TTL_SECONDS) {
|
|
3978
|
+
try {
|
|
3979
|
+
const dir = getCacheDir();
|
|
3980
|
+
await mkdir5(dir, { recursive: true });
|
|
3981
|
+
const entry = {
|
|
3982
|
+
fetchedAt: data.fetchedAt,
|
|
3983
|
+
ttl: ttlSeconds,
|
|
3984
|
+
data
|
|
3985
|
+
};
|
|
3986
|
+
await writeFile7(cacheFilePath(packageName), JSON.stringify(entry, null, 2), { encoding: "utf-8", mode: 384 });
|
|
3987
|
+
} catch {
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
var METRIC_SET_METRICS2 = {
|
|
3991
|
+
crashRateMetricSet: ["crashRate", "userPerceivedCrashRate", "distinctUsers"],
|
|
3992
|
+
anrRateMetricSet: ["anrRate", "userPerceivedAnrRate", "distinctUsers"],
|
|
3993
|
+
slowStartRateMetricSet: ["slowStartRate", "distinctUsers"],
|
|
3994
|
+
slowRenderingRateMetricSet: ["slowRenderingRate", "distinctUsers"]
|
|
3995
|
+
};
|
|
3996
|
+
var DEFAULT_THRESHOLDS = {
|
|
3997
|
+
crashRate: 0.02,
|
|
3998
|
+
anrRate: 0.01,
|
|
3999
|
+
slowStartRate: 0.05,
|
|
4000
|
+
slowRenderingRate: 0.1
|
|
4001
|
+
};
|
|
4002
|
+
var WARN_MARGIN = 0.2;
|
|
4003
|
+
function toApiDate(d) {
|
|
4004
|
+
return { year: d.getUTCFullYear(), month: d.getUTCMonth() + 1, day: d.getUTCDate() };
|
|
4005
|
+
}
|
|
4006
|
+
async function queryVitalForStatus(reporting, packageName, metricSet, days) {
|
|
4007
|
+
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
4008
|
+
const baseMs = Date.now() - 2 * DAY_MS;
|
|
4009
|
+
const end = new Date(baseMs);
|
|
4010
|
+
const start = new Date(baseMs - days * DAY_MS);
|
|
4011
|
+
const metrics = METRIC_SET_METRICS2[metricSet] ?? ["distinctUsers"];
|
|
4012
|
+
const query = {
|
|
4013
|
+
metrics,
|
|
4014
|
+
timelineSpec: {
|
|
4015
|
+
aggregationPeriod: "DAILY",
|
|
4016
|
+
startTime: toApiDate(start),
|
|
4017
|
+
endTime: toApiDate(end)
|
|
4018
|
+
}
|
|
4019
|
+
};
|
|
4020
|
+
const result = await reporting.queryMetricSet(packageName, metricSet, query);
|
|
4021
|
+
if (!result.rows || result.rows.length === 0) return void 0;
|
|
4022
|
+
const values = result.rows.map((row) => {
|
|
4023
|
+
const firstKey = Object.keys(row.metrics)[0];
|
|
4024
|
+
return firstKey ? Number(row.metrics[firstKey]?.decimalValue?.value) : NaN;
|
|
4025
|
+
}).filter((v) => !isNaN(v));
|
|
4026
|
+
if (values.length === 0) return void 0;
|
|
4027
|
+
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
4028
|
+
}
|
|
4029
|
+
function toVitalMetric(value, threshold) {
|
|
4030
|
+
if (value === void 0) {
|
|
4031
|
+
return { value: void 0, threshold, status: "unknown" };
|
|
4032
|
+
}
|
|
4033
|
+
if (value > threshold) return { value, threshold, status: "breach" };
|
|
4034
|
+
if (value > threshold * (1 - WARN_MARGIN)) return { value, threshold, status: "warn" };
|
|
4035
|
+
return { value, threshold, status: "ok" };
|
|
4036
|
+
}
|
|
4037
|
+
function computeReviewSentiment(reviews, windowDays) {
|
|
4038
|
+
const now = Date.now();
|
|
4039
|
+
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
4040
|
+
const windowMs = windowDays * DAY_MS;
|
|
4041
|
+
const prevWindowStart = now - 2 * windowMs;
|
|
4042
|
+
const curWindowStart = now - windowMs;
|
|
4043
|
+
const current = reviews.filter((r) => {
|
|
4044
|
+
const uc = r.comments?.[0]?.userComment;
|
|
4045
|
+
if (!uc) return false;
|
|
4046
|
+
const ts = Number(uc.lastModified.seconds) * 1e3;
|
|
4047
|
+
return ts >= curWindowStart;
|
|
4048
|
+
});
|
|
4049
|
+
const previous = reviews.filter((r) => {
|
|
4050
|
+
const uc = r.comments?.[0]?.userComment;
|
|
4051
|
+
if (!uc) return false;
|
|
4052
|
+
const ts = Number(uc.lastModified.seconds) * 1e3;
|
|
4053
|
+
return ts >= prevWindowStart && ts < curWindowStart;
|
|
4054
|
+
});
|
|
4055
|
+
const avgRating = (items) => {
|
|
4056
|
+
const ratings = items.map((r) => r.comments?.[0]?.userComment?.starRating).filter((v) => v !== void 0 && v > 0);
|
|
4057
|
+
if (ratings.length === 0) return void 0;
|
|
4058
|
+
return Math.round(ratings.reduce((a, b) => a + b, 0) / ratings.length * 10) / 10;
|
|
4059
|
+
};
|
|
4060
|
+
const positiveCount = current.filter(
|
|
4061
|
+
(r) => (r.comments?.[0]?.userComment?.starRating ?? 0) >= 4
|
|
4062
|
+
).length;
|
|
4063
|
+
const positivePercent = current.length > 0 ? Math.round(positiveCount / current.length * 100) : void 0;
|
|
4064
|
+
return {
|
|
4065
|
+
windowDays,
|
|
4066
|
+
averageRating: avgRating(current),
|
|
4067
|
+
previousAverageRating: avgRating(previous),
|
|
4068
|
+
totalNew: current.length,
|
|
4069
|
+
positivePercent
|
|
4070
|
+
};
|
|
4071
|
+
}
|
|
4072
|
+
async function getAppStatus(client, reporting, packageName, options = {}) {
|
|
4073
|
+
const days = options.days ?? 7;
|
|
4074
|
+
const thresholds = {
|
|
4075
|
+
crashRate: options.vitalThresholds?.crashRate ?? DEFAULT_THRESHOLDS.crashRate,
|
|
4076
|
+
anrRate: options.vitalThresholds?.anrRate ?? DEFAULT_THRESHOLDS.anrRate,
|
|
4077
|
+
slowStartRate: options.vitalThresholds?.slowStartRate ?? DEFAULT_THRESHOLDS.slowStartRate,
|
|
4078
|
+
slowRenderingRate: options.vitalThresholds?.slowRenderingRate ?? DEFAULT_THRESHOLDS.slowRenderingRate
|
|
4079
|
+
};
|
|
4080
|
+
const [releasesResult, crashesResult, anrResult, slowStartResult, slowRenderResult, reviewsResult] = await Promise.allSettled([
|
|
4081
|
+
getReleasesStatus(client, packageName),
|
|
4082
|
+
queryVitalForStatus(reporting, packageName, "crashRateMetricSet", days),
|
|
4083
|
+
queryVitalForStatus(reporting, packageName, "anrRateMetricSet", days),
|
|
4084
|
+
queryVitalForStatus(reporting, packageName, "slowStartRateMetricSet", days),
|
|
4085
|
+
queryVitalForStatus(reporting, packageName, "slowRenderingRateMetricSet", days),
|
|
4086
|
+
listReviews(client, packageName, { maxResults: 500 })
|
|
4087
|
+
]);
|
|
4088
|
+
const rawReleases = releasesResult.status === "fulfilled" ? releasesResult.value : [];
|
|
4089
|
+
const releases = rawReleases.map((r) => ({
|
|
4090
|
+
track: r.track,
|
|
4091
|
+
versionCode: r.versionCodes[r.versionCodes.length - 1] ?? "\u2014",
|
|
4092
|
+
status: r.status,
|
|
4093
|
+
userFraction: r.userFraction ?? null
|
|
4094
|
+
}));
|
|
4095
|
+
const crashValue = crashesResult.status === "fulfilled" ? crashesResult.value : void 0;
|
|
4096
|
+
const anrValue = anrResult.status === "fulfilled" ? anrResult.value : void 0;
|
|
4097
|
+
const slowStartValue = slowStartResult.status === "fulfilled" ? slowStartResult.value : void 0;
|
|
4098
|
+
const slowRenderValue = slowRenderResult.status === "fulfilled" ? slowRenderResult.value : void 0;
|
|
4099
|
+
const rawReviews = reviewsResult.status === "fulfilled" ? reviewsResult.value : [];
|
|
4100
|
+
const reviews = computeReviewSentiment(rawReviews, 30);
|
|
4101
|
+
const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4102
|
+
return {
|
|
4103
|
+
packageName,
|
|
4104
|
+
fetchedAt,
|
|
4105
|
+
cached: false,
|
|
4106
|
+
releases,
|
|
4107
|
+
vitals: {
|
|
4108
|
+
windowDays: days,
|
|
4109
|
+
crashes: toVitalMetric(crashValue, thresholds.crashRate),
|
|
4110
|
+
anr: toVitalMetric(anrValue, thresholds.anrRate),
|
|
4111
|
+
slowStarts: toVitalMetric(slowStartValue, thresholds.slowStartRate),
|
|
4112
|
+
slowRender: toVitalMetric(slowRenderValue, thresholds.slowRenderingRate)
|
|
4113
|
+
},
|
|
4114
|
+
reviews
|
|
4115
|
+
};
|
|
4116
|
+
}
|
|
4117
|
+
function vitalIndicator(metric) {
|
|
4118
|
+
if (metric.status === "unknown") return "?";
|
|
4119
|
+
if (metric.status === "breach") return "\u2717";
|
|
4120
|
+
if (metric.status === "warn") return "\u26A0";
|
|
4121
|
+
return "\u2713";
|
|
4122
|
+
}
|
|
4123
|
+
function formatVitalValue(metric) {
|
|
4124
|
+
if (metric.value === void 0) return "n/a";
|
|
4125
|
+
return `${(metric.value * 100).toFixed(2)}%`;
|
|
4126
|
+
}
|
|
4127
|
+
function formatFraction(fraction) {
|
|
4128
|
+
if (fraction === null) return "\u2014";
|
|
4129
|
+
return `${Math.round(fraction * 100)}%`;
|
|
4130
|
+
}
|
|
4131
|
+
function formatRating(rating) {
|
|
4132
|
+
if (rating === void 0) return "n/a";
|
|
4133
|
+
return `\u2605 ${rating.toFixed(1)}`;
|
|
4134
|
+
}
|
|
4135
|
+
function formatTrend(current, previous) {
|
|
4136
|
+
if (current === void 0 || previous === void 0) return "";
|
|
4137
|
+
if (current > previous) return ` \u2191 from ${previous.toFixed(1)}`;
|
|
4138
|
+
if (current < previous) return ` \u2193 from ${previous.toFixed(1)}`;
|
|
4139
|
+
return "";
|
|
4140
|
+
}
|
|
4141
|
+
function formatStatusTable(status) {
|
|
4142
|
+
const lines = [];
|
|
4143
|
+
const cachedLabel = status.cached ? ` (cached ${new Date(status.fetchedAt).toLocaleTimeString()})` : ` (fetched ${new Date(status.fetchedAt).toLocaleTimeString()})`;
|
|
4144
|
+
lines.push(`App: ${status.packageName}${cachedLabel}`);
|
|
4145
|
+
lines.push("");
|
|
4146
|
+
lines.push("RELEASES");
|
|
4147
|
+
if (status.releases.length === 0) {
|
|
4148
|
+
lines.push(" No releases found.");
|
|
4149
|
+
} else {
|
|
4150
|
+
const trackW = Math.max(10, ...status.releases.map((r) => r.track.length));
|
|
4151
|
+
const versionW = Math.max(7, ...status.releases.map((r) => r.versionCode.length));
|
|
4152
|
+
const statusW = Math.max(8, ...status.releases.map((r) => r.status.length));
|
|
4153
|
+
for (const r of status.releases) {
|
|
4154
|
+
lines.push(
|
|
4155
|
+
` ${r.track.padEnd(trackW)} ${r.versionCode.padEnd(versionW)} ${r.status.padEnd(statusW)} ${formatFraction(r.userFraction)}`
|
|
4156
|
+
);
|
|
4157
|
+
}
|
|
4158
|
+
}
|
|
4159
|
+
lines.push("");
|
|
4160
|
+
lines.push(`VITALS (last ${status.vitals.windowDays} days)`);
|
|
4161
|
+
const { crashes, anr, slowStarts, slowRender } = status.vitals;
|
|
4162
|
+
lines.push(
|
|
4163
|
+
` crashes ${formatVitalValue(crashes).padEnd(8)} ${vitalIndicator(crashes)} anr ${formatVitalValue(anr).padEnd(8)} ${vitalIndicator(anr)}`
|
|
4164
|
+
);
|
|
4165
|
+
lines.push(
|
|
4166
|
+
` slow starts ${formatVitalValue(slowStarts).padEnd(8)} ${vitalIndicator(slowStarts)} slow render ${formatVitalValue(slowRender).padEnd(8)} ${vitalIndicator(slowRender)}`
|
|
4167
|
+
);
|
|
4168
|
+
lines.push("");
|
|
4169
|
+
lines.push(`REVIEWS (last ${status.reviews.windowDays} days)`);
|
|
4170
|
+
const { averageRating, previousAverageRating, totalNew, positivePercent } = status.reviews;
|
|
4171
|
+
const trend = formatTrend(averageRating, previousAverageRating);
|
|
4172
|
+
const positiveStr = positivePercent !== void 0 ? ` ${positivePercent}% positive` : "";
|
|
4173
|
+
lines.push(
|
|
4174
|
+
` ${formatRating(averageRating)} ${totalNew} new${positiveStr}${trend}`
|
|
4175
|
+
);
|
|
4176
|
+
return lines.join("\n");
|
|
4177
|
+
}
|
|
4178
|
+
function statusHasBreach(status) {
|
|
4179
|
+
return status.vitals.crashes.status === "breach" || status.vitals.anr.status === "breach" || status.vitals.slowStarts.status === "breach" || status.vitals.slowRender.status === "breach";
|
|
4180
|
+
}
|
|
3762
4181
|
export {
|
|
3763
4182
|
ApiError,
|
|
3764
4183
|
ConfigError,
|
|
@@ -3775,11 +4194,13 @@ export {
|
|
|
3775
4194
|
activatePurchaseOption,
|
|
3776
4195
|
addRecoveryTargeting,
|
|
3777
4196
|
addTesters,
|
|
4197
|
+
analyzeBundle,
|
|
3778
4198
|
batchSyncInAppProducts,
|
|
3779
4199
|
cancelRecoveryAction,
|
|
3780
4200
|
cancelSubscriptionPurchase,
|
|
3781
4201
|
checkThreshold,
|
|
3782
4202
|
clearAuditLog,
|
|
4203
|
+
compareBundles,
|
|
3783
4204
|
compareVitalsTrend,
|
|
3784
4205
|
consumeProductPurchase,
|
|
3785
4206
|
convertRegionPrices,
|
|
@@ -3826,9 +4247,11 @@ export {
|
|
|
3826
4247
|
formatJunit,
|
|
3827
4248
|
formatOutput,
|
|
3828
4249
|
formatSlackPayload,
|
|
4250
|
+
formatStatusTable,
|
|
3829
4251
|
generateMigrationPlan,
|
|
3830
4252
|
generateNotesFromGit,
|
|
3831
4253
|
getAppInfo,
|
|
4254
|
+
getAppStatus,
|
|
3832
4255
|
getCountryAvailability,
|
|
3833
4256
|
getDataSafety,
|
|
3834
4257
|
getDeviceTier,
|
|
@@ -3879,6 +4302,7 @@ export {
|
|
|
3879
4302
|
listTracks,
|
|
3880
4303
|
listUsers,
|
|
3881
4304
|
listVoidedPurchases,
|
|
4305
|
+
loadStatusCache,
|
|
3882
4306
|
migratePrices,
|
|
3883
4307
|
parseAppfile,
|
|
3884
4308
|
parseFastfile,
|
|
@@ -3900,11 +4324,13 @@ export {
|
|
|
3900
4324
|
revokeSubscriptionPurchase,
|
|
3901
4325
|
safePath,
|
|
3902
4326
|
safePathWithin,
|
|
4327
|
+
saveStatusCache,
|
|
3903
4328
|
scaffoldPlugin,
|
|
3904
4329
|
searchAuditEvents,
|
|
3905
4330
|
searchVitalsErrors,
|
|
3906
4331
|
sendWebhook,
|
|
3907
4332
|
sortResults,
|
|
4333
|
+
statusHasBreach,
|
|
3908
4334
|
syncInAppProducts,
|
|
3909
4335
|
updateAppDetails,
|
|
3910
4336
|
updateDataSafety,
|