@gpc-cli/core 0.9.73 → 0.9.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +82 -2
- package/dist/index.js +534 -205
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1247,8 +1247,8 @@ var ALL_IMAGE_TYPES = [
|
|
|
1247
1247
|
"tvBanner"
|
|
1248
1248
|
];
|
|
1249
1249
|
async function exportImages(client, packageName, dir, options) {
|
|
1250
|
-
const { mkdir:
|
|
1251
|
-
const { join:
|
|
1250
|
+
const { mkdir: mkdir9, writeFile: writeFile10 } = await import("fs/promises");
|
|
1251
|
+
const { join: join13 } = await import("path");
|
|
1252
1252
|
const edit = await client.edits.insert(packageName);
|
|
1253
1253
|
try {
|
|
1254
1254
|
let languages;
|
|
@@ -1279,8 +1279,8 @@ async function exportImages(client, packageName, dir, options) {
|
|
|
1279
1279
|
const batch = tasks.slice(i, i + concurrency);
|
|
1280
1280
|
const results = await Promise.all(
|
|
1281
1281
|
batch.map(async (task) => {
|
|
1282
|
-
const dirPath =
|
|
1283
|
-
await
|
|
1282
|
+
const dirPath = join13(dir, task.language, task.imageType);
|
|
1283
|
+
await mkdir9(dirPath, { recursive: true });
|
|
1284
1284
|
const response = await fetch(task.url);
|
|
1285
1285
|
if (!response.ok) {
|
|
1286
1286
|
throw new GpcError(
|
|
@@ -1291,8 +1291,8 @@ async function exportImages(client, packageName, dir, options) {
|
|
|
1291
1291
|
);
|
|
1292
1292
|
}
|
|
1293
1293
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
1294
|
-
const filePath =
|
|
1295
|
-
await
|
|
1294
|
+
const filePath = join13(dirPath, `${task.index}.png`);
|
|
1295
|
+
await writeFile10(filePath, buffer);
|
|
1296
1296
|
return buffer.length;
|
|
1297
1297
|
})
|
|
1298
1298
|
);
|
|
@@ -1983,7 +1983,67 @@ function analyzeReviews(reviews) {
|
|
|
1983
1983
|
};
|
|
1984
1984
|
}
|
|
1985
1985
|
|
|
1986
|
+
// src/utils/sort.ts
|
|
1987
|
+
function getNestedValue(obj, path) {
|
|
1988
|
+
const parts = path.split(".");
|
|
1989
|
+
let current = obj;
|
|
1990
|
+
for (const part of parts) {
|
|
1991
|
+
if (current === null || current === void 0 || typeof current !== "object") {
|
|
1992
|
+
return void 0;
|
|
1993
|
+
}
|
|
1994
|
+
current = current[part];
|
|
1995
|
+
}
|
|
1996
|
+
return current;
|
|
1997
|
+
}
|
|
1998
|
+
function compare(a, b) {
|
|
1999
|
+
if (a === void 0 && b === void 0) return 0;
|
|
2000
|
+
if (a === void 0) return 1;
|
|
2001
|
+
if (b === void 0) return -1;
|
|
2002
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
2003
|
+
return a - b;
|
|
2004
|
+
}
|
|
2005
|
+
return String(a).localeCompare(String(b));
|
|
2006
|
+
}
|
|
2007
|
+
function sortResults(items, sortSpec) {
|
|
2008
|
+
if (!sortSpec || items.length === 0) {
|
|
2009
|
+
return items;
|
|
2010
|
+
}
|
|
2011
|
+
const descending = sortSpec.startsWith("-");
|
|
2012
|
+
const field = descending ? sortSpec.slice(1) : sortSpec;
|
|
2013
|
+
const hasField = items.some((item) => getNestedValue(item, field) !== void 0);
|
|
2014
|
+
if (!hasField) {
|
|
2015
|
+
return items;
|
|
2016
|
+
}
|
|
2017
|
+
const sorted = [...items].sort((a, b) => {
|
|
2018
|
+
const aVal = getNestedValue(a, field);
|
|
2019
|
+
const bVal = getNestedValue(b, field);
|
|
2020
|
+
const result = compare(aVal, bVal);
|
|
2021
|
+
return descending ? -result : result;
|
|
2022
|
+
});
|
|
2023
|
+
return sorted;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1986
2026
|
// src/commands/reviews.ts
|
|
2027
|
+
var REVIEW_SORT_PRESETS = ["newest", "oldest", "rating"];
|
|
2028
|
+
function reviewTimestamp(r) {
|
|
2029
|
+
const secs = Number(r.comments?.[0]?.userComment?.lastModified?.seconds);
|
|
2030
|
+
return Number.isFinite(secs) ? secs : 0;
|
|
2031
|
+
}
|
|
2032
|
+
function reviewRating(r) {
|
|
2033
|
+
return r.comments?.[0]?.userComment?.starRating ?? 0;
|
|
2034
|
+
}
|
|
2035
|
+
function sortReviews(reviews, spec) {
|
|
2036
|
+
switch (spec) {
|
|
2037
|
+
case "newest":
|
|
2038
|
+
return [...reviews].sort((a, b) => reviewTimestamp(b) - reviewTimestamp(a));
|
|
2039
|
+
case "oldest":
|
|
2040
|
+
return [...reviews].sort((a, b) => reviewTimestamp(a) - reviewTimestamp(b));
|
|
2041
|
+
case "rating":
|
|
2042
|
+
return [...reviews].sort((a, b) => reviewRating(b) - reviewRating(a));
|
|
2043
|
+
default:
|
|
2044
|
+
return sortResults(reviews, spec);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
1987
2047
|
async function listReviews(client, packageName, options) {
|
|
1988
2048
|
let reviews;
|
|
1989
2049
|
let nextPageToken;
|
|
@@ -2918,6 +2978,19 @@ async function getAppStatus(client, reporting, packageName, options = {}) {
|
|
|
2918
2978
|
if (reviewsResult.status === "rejected")
|
|
2919
2979
|
sectionErrors.push(`reviews: ${String(reviewsResult.reason)}`);
|
|
2920
2980
|
const reviews = computeReviewSentiment(rawReviews, reviewDays);
|
|
2981
|
+
if (options.full && sections.has("reviews")) {
|
|
2982
|
+
const windowStartSec = (Date.now() - reviewDays * 24 * 60 * 60 * 1e3) / 1e3;
|
|
2983
|
+
const items = rawReviews.filter((r) => {
|
|
2984
|
+
const secs = Number(r.comments?.[0]?.userComment?.lastModified?.seconds);
|
|
2985
|
+
return Number.isFinite(secs) && secs >= windowStartSec;
|
|
2986
|
+
}).map((r) => {
|
|
2987
|
+
const uc = r.comments?.[0]?.userComment;
|
|
2988
|
+
return { text: uc?.text ?? "", rating: uc?.starRating };
|
|
2989
|
+
}).filter((r) => r.text.length > 0);
|
|
2990
|
+
if (items.length > 0) {
|
|
2991
|
+
reviews.analysis = analyzeReviews(items);
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2921
2994
|
return {
|
|
2922
2995
|
packageName,
|
|
2923
2996
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -3040,10 +3113,31 @@ function formatStatusTable(status) {
|
|
|
3040
3113
|
const trend = formatTrend(averageRating, previousAverageRating);
|
|
3041
3114
|
const positiveStr = positivePercent !== void 0 ? ` ${positivePercent}% positive` : "";
|
|
3042
3115
|
lines.push(` ${formatRating(averageRating)} ${totalNew} new${positiveStr}${trend}`);
|
|
3116
|
+
lines.push(...formatReviewInsights(status.reviews.analysis));
|
|
3043
3117
|
}
|
|
3044
3118
|
}
|
|
3045
3119
|
return lines.join("\n");
|
|
3046
3120
|
}
|
|
3121
|
+
function formatReviewInsights(analysis) {
|
|
3122
|
+
if (!analysis || analysis.totalReviews === 0) return [];
|
|
3123
|
+
const total = analysis.totalReviews;
|
|
3124
|
+
const pct = (n) => Math.round(n / total * 100);
|
|
3125
|
+
const s = analysis.sentiment;
|
|
3126
|
+
const lines = [
|
|
3127
|
+
"",
|
|
3128
|
+
" REVIEW INSIGHTS",
|
|
3129
|
+
` text sentiment ${pct(s.positive)}% positive ${pct(s.negative)}% negative ${pct(s.neutral)}% neutral`
|
|
3130
|
+
];
|
|
3131
|
+
if (analysis.topics.length > 0) {
|
|
3132
|
+
lines.push(
|
|
3133
|
+
` top topics ${analysis.topics.slice(0, 5).map((t) => `${t.topic} (${t.count})`).join(", ")}`
|
|
3134
|
+
);
|
|
3135
|
+
}
|
|
3136
|
+
if (analysis.keywords.length > 0) {
|
|
3137
|
+
lines.push(` keywords ${analysis.keywords.slice(0, 8).map((k) => k.word).join(", ")}`);
|
|
3138
|
+
}
|
|
3139
|
+
return lines;
|
|
3140
|
+
}
|
|
3047
3141
|
function formatStatusSummary(status) {
|
|
3048
3142
|
const parts = [status.packageName];
|
|
3049
3143
|
const latestRelease = status.releases.find((r) => r.status !== "draft") ?? status.releases[0];
|
|
@@ -4607,6 +4701,8 @@ async function executeStage(apiClient, packageName, state, stageIndex) {
|
|
|
4607
4701
|
}
|
|
4608
4702
|
|
|
4609
4703
|
// src/commands/games.ts
|
|
4704
|
+
import { mkdir as mkdir5, readdir as readdir4, readFile as readFile8, writeFile as writeFile5 } from "fs/promises";
|
|
4705
|
+
import { extname as extname2, join as join6 } from "path";
|
|
4610
4706
|
async function listLeaderboards(client, packageName) {
|
|
4611
4707
|
const result = await client.leaderboards.list(packageName);
|
|
4612
4708
|
return result.items ?? [];
|
|
@@ -4707,6 +4803,141 @@ async function diffLeaderboardConfig(client, leaderboardId, localData) {
|
|
|
4707
4803
|
}
|
|
4708
4804
|
return diffs;
|
|
4709
4805
|
}
|
|
4806
|
+
var ICON_CONTENT_TYPES = {
|
|
4807
|
+
".png": "image/png",
|
|
4808
|
+
".jpg": "image/jpeg",
|
|
4809
|
+
".jpeg": "image/jpeg"
|
|
4810
|
+
};
|
|
4811
|
+
function iconContentType(filePath) {
|
|
4812
|
+
const ext = extname2(filePath).toLowerCase();
|
|
4813
|
+
const contentType = ICON_CONTENT_TYPES[ext];
|
|
4814
|
+
if (!contentType) {
|
|
4815
|
+
throw new GpcError(
|
|
4816
|
+
`Unsupported icon format: ${ext || "(no extension)"}`,
|
|
4817
|
+
"GAMES_INVALID_ICON_FORMAT",
|
|
4818
|
+
2,
|
|
4819
|
+
"Provide a .png, .jpg, or .jpeg image (Play Games icons are 512x512)."
|
|
4820
|
+
);
|
|
4821
|
+
}
|
|
4822
|
+
return contentType;
|
|
4823
|
+
}
|
|
4824
|
+
async function setAchievementIcon(client, achievementId, filePath) {
|
|
4825
|
+
validateResourceId(achievementId, "achievement");
|
|
4826
|
+
return client.images.upload(achievementId, "ACHIEVEMENT_ICON", filePath, iconContentType(filePath));
|
|
4827
|
+
}
|
|
4828
|
+
async function setLeaderboardIcon(client, leaderboardId, filePath) {
|
|
4829
|
+
validateResourceId(leaderboardId, "leaderboard");
|
|
4830
|
+
return client.images.upload(leaderboardId, "LEADERBOARD_ICON", filePath, iconContentType(filePath));
|
|
4831
|
+
}
|
|
4832
|
+
function safeConfigFilename(id) {
|
|
4833
|
+
return `${id.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`;
|
|
4834
|
+
}
|
|
4835
|
+
async function readConfigDir(dir) {
|
|
4836
|
+
const entries = (await readdir4(dir)).filter((f) => f.toLowerCase().endsWith(".json"));
|
|
4837
|
+
const out = [];
|
|
4838
|
+
for (const file of entries) {
|
|
4839
|
+
const raw = await readFile8(join6(dir, file), "utf-8");
|
|
4840
|
+
let parsed;
|
|
4841
|
+
try {
|
|
4842
|
+
parsed = JSON.parse(raw);
|
|
4843
|
+
} catch (err) {
|
|
4844
|
+
throw new GpcError(
|
|
4845
|
+
`Invalid JSON in ${file}: ${err instanceof Error ? err.message : String(err)}`,
|
|
4846
|
+
"GAMES_INVALID_CONFIG_JSON",
|
|
4847
|
+
2,
|
|
4848
|
+
"Each file in the sync directory must be a valid config JSON."
|
|
4849
|
+
);
|
|
4850
|
+
}
|
|
4851
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
4852
|
+
throw new GpcError(
|
|
4853
|
+
`${file} is not a config object.`,
|
|
4854
|
+
"GAMES_INVALID_CONFIG_JSON",
|
|
4855
|
+
2,
|
|
4856
|
+
"Each file must contain a single achievement or leaderboard config object."
|
|
4857
|
+
);
|
|
4858
|
+
}
|
|
4859
|
+
out.push({ file, config: parsed });
|
|
4860
|
+
}
|
|
4861
|
+
return out;
|
|
4862
|
+
}
|
|
4863
|
+
async function pushAchievementConfigs(client, applicationId, dir, options) {
|
|
4864
|
+
validateApplicationId(applicationId);
|
|
4865
|
+
const created = [];
|
|
4866
|
+
const updated = [];
|
|
4867
|
+
for (const { file, config } of await readConfigDir(dir)) {
|
|
4868
|
+
if (config.id) {
|
|
4869
|
+
if (!options?.dryRun) await client.achievements.update(config.id, config);
|
|
4870
|
+
updated.push(config.id);
|
|
4871
|
+
} else {
|
|
4872
|
+
if (options?.dryRun) {
|
|
4873
|
+
created.push(file);
|
|
4874
|
+
} else {
|
|
4875
|
+
const res = await client.achievements.insert(applicationId, config);
|
|
4876
|
+
created.push(res.id ?? file);
|
|
4877
|
+
}
|
|
4878
|
+
}
|
|
4879
|
+
}
|
|
4880
|
+
return { created, updated };
|
|
4881
|
+
}
|
|
4882
|
+
async function pullAchievementConfigs(client, applicationId, dir) {
|
|
4883
|
+
validateApplicationId(applicationId);
|
|
4884
|
+
await mkdir5(dir, { recursive: true });
|
|
4885
|
+
const written = [];
|
|
4886
|
+
let pageToken;
|
|
4887
|
+
do {
|
|
4888
|
+
const result = await client.achievements.list(applicationId, { maxResults: 200, pageToken });
|
|
4889
|
+
for (const config of result.items ?? []) {
|
|
4890
|
+
const id = config.id ?? "unknown";
|
|
4891
|
+
await writeFile5(
|
|
4892
|
+
join6(dir, safeConfigFilename(id)),
|
|
4893
|
+
JSON.stringify(config, null, 2) + "\n",
|
|
4894
|
+
"utf-8"
|
|
4895
|
+
);
|
|
4896
|
+
written.push(id);
|
|
4897
|
+
}
|
|
4898
|
+
pageToken = result.nextPageToken;
|
|
4899
|
+
} while (pageToken);
|
|
4900
|
+
return written;
|
|
4901
|
+
}
|
|
4902
|
+
async function pushLeaderboardConfigs(client, applicationId, dir, options) {
|
|
4903
|
+
validateApplicationId(applicationId);
|
|
4904
|
+
const created = [];
|
|
4905
|
+
const updated = [];
|
|
4906
|
+
for (const { file, config } of await readConfigDir(dir)) {
|
|
4907
|
+
if (config.id) {
|
|
4908
|
+
if (!options?.dryRun) await client.leaderboards.update(config.id, config);
|
|
4909
|
+
updated.push(config.id);
|
|
4910
|
+
} else {
|
|
4911
|
+
if (options?.dryRun) {
|
|
4912
|
+
created.push(file);
|
|
4913
|
+
} else {
|
|
4914
|
+
const res = await client.leaderboards.insert(applicationId, config);
|
|
4915
|
+
created.push(res.id ?? file);
|
|
4916
|
+
}
|
|
4917
|
+
}
|
|
4918
|
+
}
|
|
4919
|
+
return { created, updated };
|
|
4920
|
+
}
|
|
4921
|
+
async function pullLeaderboardConfigs(client, applicationId, dir) {
|
|
4922
|
+
validateApplicationId(applicationId);
|
|
4923
|
+
await mkdir5(dir, { recursive: true });
|
|
4924
|
+
const written = [];
|
|
4925
|
+
let pageToken;
|
|
4926
|
+
do {
|
|
4927
|
+
const result = await client.leaderboards.list(applicationId, { maxResults: 200, pageToken });
|
|
4928
|
+
for (const config of result.items ?? []) {
|
|
4929
|
+
const id = config.id ?? "unknown";
|
|
4930
|
+
await writeFile5(
|
|
4931
|
+
join6(dir, safeConfigFilename(id)),
|
|
4932
|
+
JSON.stringify(config, null, 2) + "\n",
|
|
4933
|
+
"utf-8"
|
|
4934
|
+
);
|
|
4935
|
+
written.push(id);
|
|
4936
|
+
}
|
|
4937
|
+
pageToken = result.nextPageToken;
|
|
4938
|
+
} while (pageToken);
|
|
4939
|
+
return written;
|
|
4940
|
+
}
|
|
4710
4941
|
|
|
4711
4942
|
// src/commands/enterprise.ts
|
|
4712
4943
|
async function createEnterpriseApp(client, params) {
|
|
@@ -4721,8 +4952,8 @@ async function publishEnterpriseApp(client, params) {
|
|
|
4721
4952
|
}
|
|
4722
4953
|
|
|
4723
4954
|
// src/audit.ts
|
|
4724
|
-
import { appendFile, chmod, mkdir as
|
|
4725
|
-
import { join as
|
|
4955
|
+
import { appendFile, chmod, mkdir as mkdir6, readFile as readFile9, writeFile as writeFile6 } from "fs/promises";
|
|
4956
|
+
import { join as join7 } from "path";
|
|
4726
4957
|
var auditDir = null;
|
|
4727
4958
|
function initAudit(configDir) {
|
|
4728
4959
|
auditDir = configDir;
|
|
@@ -4730,8 +4961,8 @@ function initAudit(configDir) {
|
|
|
4730
4961
|
async function writeAuditLog(entry) {
|
|
4731
4962
|
if (!auditDir) return;
|
|
4732
4963
|
try {
|
|
4733
|
-
await
|
|
4734
|
-
const logPath =
|
|
4964
|
+
await mkdir6(auditDir, { recursive: true, mode: 448 });
|
|
4965
|
+
const logPath = join7(auditDir, "audit.log");
|
|
4735
4966
|
const redactedEntry = redactAuditArgs(entry);
|
|
4736
4967
|
const line = JSON.stringify(redactedEntry) + "\n";
|
|
4737
4968
|
await appendFile(logPath, line, { encoding: "utf-8", mode: 384 });
|
|
@@ -4786,10 +5017,10 @@ function createAuditEntry(command, args, app) {
|
|
|
4786
5017
|
}
|
|
4787
5018
|
async function listAuditEvents(options) {
|
|
4788
5019
|
if (!auditDir) return [];
|
|
4789
|
-
const logPath =
|
|
5020
|
+
const logPath = join7(auditDir, "audit.log");
|
|
4790
5021
|
let content;
|
|
4791
5022
|
try {
|
|
4792
|
-
content = await
|
|
5023
|
+
content = await readFile9(logPath, "utf-8");
|
|
4793
5024
|
} catch {
|
|
4794
5025
|
return [];
|
|
4795
5026
|
}
|
|
@@ -4824,10 +5055,10 @@ async function searchAuditEvents(query) {
|
|
|
4824
5055
|
}
|
|
4825
5056
|
async function clearAuditLog(options) {
|
|
4826
5057
|
if (!auditDir) return { deleted: 0, remaining: 0 };
|
|
4827
|
-
const logPath =
|
|
5058
|
+
const logPath = join7(auditDir, "audit.log");
|
|
4828
5059
|
let content;
|
|
4829
5060
|
try {
|
|
4830
|
-
content = await
|
|
5061
|
+
content = await readFile9(logPath, "utf-8");
|
|
4831
5062
|
} catch {
|
|
4832
5063
|
return { deleted: 0, remaining: 0 };
|
|
4833
5064
|
}
|
|
@@ -4835,7 +5066,7 @@ async function clearAuditLog(options) {
|
|
|
4835
5066
|
if (!options?.before) {
|
|
4836
5067
|
const count = lines.length;
|
|
4837
5068
|
if (!options?.dryRun) {
|
|
4838
|
-
await
|
|
5069
|
+
await writeFile6(logPath, "", { encoding: "utf-8", mode: 384 });
|
|
4839
5070
|
}
|
|
4840
5071
|
return { deleted: count, remaining: 0 };
|
|
4841
5072
|
}
|
|
@@ -4855,7 +5086,7 @@ async function clearAuditLog(options) {
|
|
|
4855
5086
|
}
|
|
4856
5087
|
}
|
|
4857
5088
|
if (!options?.dryRun) {
|
|
4858
|
-
await
|
|
5089
|
+
await writeFile6(logPath, keep.length > 0 ? keep.join("\n") + "\n" : "", {
|
|
4859
5090
|
encoding: "utf-8",
|
|
4860
5091
|
mode: 384
|
|
4861
5092
|
});
|
|
@@ -4915,8 +5146,8 @@ function safePathWithin(userPath, baseDir) {
|
|
|
4915
5146
|
}
|
|
4916
5147
|
|
|
4917
5148
|
// src/commands/init.ts
|
|
4918
|
-
import { mkdir as
|
|
4919
|
-
import { join as
|
|
5149
|
+
import { mkdir as mkdir7, writeFile as writeFile7, stat as stat5 } from "fs/promises";
|
|
5150
|
+
import { join as join8 } from "path";
|
|
4920
5151
|
async function exists2(path) {
|
|
4921
5152
|
try {
|
|
4922
5153
|
await stat5(path);
|
|
@@ -4933,8 +5164,8 @@ async function safeWrite(filePath, content, created, skipped, skipExisting) {
|
|
|
4933
5164
|
}
|
|
4934
5165
|
}
|
|
4935
5166
|
const dir = filePath.substring(0, filePath.lastIndexOf("/"));
|
|
4936
|
-
await
|
|
4937
|
-
await
|
|
5167
|
+
await mkdir7(dir, { recursive: true });
|
|
5168
|
+
await writeFile7(filePath, content, "utf-8");
|
|
4938
5169
|
created.push(filePath);
|
|
4939
5170
|
}
|
|
4940
5171
|
async function initProject(options) {
|
|
@@ -4951,7 +5182,7 @@ async function initProject(options) {
|
|
|
4951
5182
|
null,
|
|
4952
5183
|
2
|
|
4953
5184
|
);
|
|
4954
|
-
await safeWrite(
|
|
5185
|
+
await safeWrite(join8(dir, ".gpcrc.json"), gpcrc + "\n", created, skipped, skipExisting);
|
|
4955
5186
|
const preflightrc = JSON.stringify(
|
|
4956
5187
|
{
|
|
4957
5188
|
failOn: "error",
|
|
@@ -4965,27 +5196,27 @@ async function initProject(options) {
|
|
|
4965
5196
|
2
|
|
4966
5197
|
);
|
|
4967
5198
|
await safeWrite(
|
|
4968
|
-
|
|
5199
|
+
join8(dir, ".preflightrc.json"),
|
|
4969
5200
|
preflightrc + "\n",
|
|
4970
5201
|
created,
|
|
4971
5202
|
skipped,
|
|
4972
5203
|
skipExisting
|
|
4973
5204
|
);
|
|
4974
|
-
const metaDir =
|
|
4975
|
-
await safeWrite(
|
|
4976
|
-
await safeWrite(
|
|
4977
|
-
await safeWrite(
|
|
4978
|
-
await safeWrite(
|
|
4979
|
-
const ssDir =
|
|
4980
|
-
await
|
|
4981
|
-
await safeWrite(
|
|
5205
|
+
const metaDir = join8(dir, "metadata", "android", "en-US");
|
|
5206
|
+
await safeWrite(join8(metaDir, "title.txt"), "", created, skipped, skipExisting);
|
|
5207
|
+
await safeWrite(join8(metaDir, "short_description.txt"), "", created, skipped, skipExisting);
|
|
5208
|
+
await safeWrite(join8(metaDir, "full_description.txt"), "", created, skipped, skipExisting);
|
|
5209
|
+
await safeWrite(join8(metaDir, "video.txt"), "", created, skipped, skipExisting);
|
|
5210
|
+
const ssDir = join8(metaDir, "images", "phoneScreenshots");
|
|
5211
|
+
await mkdir7(ssDir, { recursive: true });
|
|
5212
|
+
await safeWrite(join8(ssDir, ".gitkeep"), "", created, skipped, skipExisting);
|
|
4982
5213
|
if (ci === "github") {
|
|
4983
5214
|
const workflow = githubActionsTemplate(pkg);
|
|
4984
|
-
const workflowDir =
|
|
4985
|
-
await safeWrite(
|
|
5215
|
+
const workflowDir = join8(dir, ".github", "workflows");
|
|
5216
|
+
await safeWrite(join8(workflowDir, "gpc-release.yml"), workflow, created, skipped, skipExisting);
|
|
4986
5217
|
} else if (ci === "gitlab") {
|
|
4987
5218
|
const pipeline = gitlabCiTemplate(pkg);
|
|
4988
|
-
await safeWrite(
|
|
5219
|
+
await safeWrite(join8(dir, ".gitlab-ci-gpc.yml"), pipeline, created, skipped, skipExisting);
|
|
4989
5220
|
}
|
|
4990
5221
|
return { created, skipped };
|
|
4991
5222
|
}
|
|
@@ -5081,13 +5312,13 @@ var DEFAULT_PREFLIGHT_CONFIG = {
|
|
|
5081
5312
|
};
|
|
5082
5313
|
|
|
5083
5314
|
// src/preflight/config.ts
|
|
5084
|
-
import { readFile as
|
|
5315
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
5085
5316
|
var VALID_SEVERITIES = /* @__PURE__ */ new Set(["critical", "error", "warning", "info"]);
|
|
5086
5317
|
async function loadPreflightConfig(configPath) {
|
|
5087
5318
|
const path = configPath || ".preflightrc.json";
|
|
5088
5319
|
let raw;
|
|
5089
5320
|
try {
|
|
5090
|
-
raw = await
|
|
5321
|
+
raw = await readFile10(path, "utf-8");
|
|
5091
5322
|
} catch {
|
|
5092
5323
|
return { ...DEFAULT_PREFLIGHT_CONFIG };
|
|
5093
5324
|
}
|
|
@@ -5132,7 +5363,8 @@ async function loadPreflightConfig(configPath) {
|
|
|
5132
5363
|
}
|
|
5133
5364
|
|
|
5134
5365
|
// src/preflight/aab-reader.ts
|
|
5135
|
-
import {
|
|
5366
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
5367
|
+
import { fromBuffer } from "yauzl";
|
|
5136
5368
|
|
|
5137
5369
|
// src/preflight/manifest-parser.ts
|
|
5138
5370
|
import protobuf from "protobufjs";
|
|
@@ -5321,12 +5553,20 @@ var AAB_MANIFEST_PATH = "base/manifest/AndroidManifest.xml";
|
|
|
5321
5553
|
var APK_MANIFEST_PATH = "AndroidManifest.xml";
|
|
5322
5554
|
var SO_HEADER_BYTES = 4096;
|
|
5323
5555
|
var SO_PATH_RE = /(?:^|\/)lib\/[^/]+\/[^/]+\.so$/;
|
|
5556
|
+
var SCAN_TIMEOUT_MS = 6e4;
|
|
5324
5557
|
function detectManifestPath(filePath) {
|
|
5325
5558
|
return filePath.toLowerCase().endsWith(".apk") ? APK_MANIFEST_PATH : AAB_MANIFEST_PATH;
|
|
5326
5559
|
}
|
|
5327
5560
|
async function readAab(aabPath) {
|
|
5328
5561
|
const manifestPath = detectManifestPath(aabPath);
|
|
5329
|
-
|
|
5562
|
+
let buffer;
|
|
5563
|
+
try {
|
|
5564
|
+
buffer = await readFile11(aabPath);
|
|
5565
|
+
} catch (err) {
|
|
5566
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5567
|
+
throw new Error(`Could not read ${aabPath}: ${msg}`, { cause: err });
|
|
5568
|
+
}
|
|
5569
|
+
const { zipfile, entries, manifestBuf, soHeaders } = await openAndScan(buffer, manifestPath);
|
|
5330
5570
|
zipfile.close();
|
|
5331
5571
|
if (!manifestBuf) {
|
|
5332
5572
|
const fileType = aabPath.toLowerCase().endsWith(".apk") ? "APK" : "AAB";
|
|
@@ -5363,98 +5603,125 @@ function createFallbackManifest() {
|
|
|
5363
5603
|
providers: []
|
|
5364
5604
|
};
|
|
5365
5605
|
}
|
|
5366
|
-
function openAndScan(
|
|
5606
|
+
function openAndScan(buffer, manifestPath = AAB_MANIFEST_PATH) {
|
|
5367
5607
|
return new Promise((resolve2, reject) => {
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5608
|
+
let settled = false;
|
|
5609
|
+
const timer = setTimeout(() => {
|
|
5610
|
+
if (!settled) {
|
|
5611
|
+
settled = true;
|
|
5612
|
+
reject(new Error("Timed out reading the archive. The file may be corrupt or truncated."));
|
|
5372
5613
|
}
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5614
|
+
}, SCAN_TIMEOUT_MS);
|
|
5615
|
+
function settleResolve(value) {
|
|
5616
|
+
if (settled) return;
|
|
5617
|
+
settled = true;
|
|
5618
|
+
clearTimeout(timer);
|
|
5619
|
+
resolve2(value);
|
|
5620
|
+
}
|
|
5621
|
+
function settleReject(error) {
|
|
5622
|
+
if (settled) return;
|
|
5623
|
+
settled = true;
|
|
5624
|
+
clearTimeout(timer);
|
|
5625
|
+
reject(error);
|
|
5626
|
+
}
|
|
5627
|
+
try {
|
|
5628
|
+
fromBuffer(buffer, { lazyEntries: true }, (err, zipfile) => {
|
|
5629
|
+
if (err || !zipfile) {
|
|
5630
|
+
settleReject(err ?? new Error("Failed to open AAB file"));
|
|
5631
|
+
return;
|
|
5384
5632
|
}
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5633
|
+
const entries = [];
|
|
5634
|
+
const soHeaders = /* @__PURE__ */ new Map();
|
|
5635
|
+
let manifestBuf = null;
|
|
5636
|
+
let pendingStreams = 0;
|
|
5637
|
+
let entrysDone = false;
|
|
5638
|
+
function fail(error) {
|
|
5639
|
+
if (settled) return;
|
|
5640
|
+
try {
|
|
5641
|
+
zipfile.close();
|
|
5642
|
+
} catch {
|
|
5643
|
+
}
|
|
5644
|
+
settleReject(error);
|
|
5389
5645
|
}
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
zipfile.openReadStream(entry, (streamErr, stream) => {
|
|
5394
|
-
if (streamErr || !stream) {
|
|
5395
|
-
fail(streamErr ?? new Error(`Failed to read ${entry.fileName}`));
|
|
5396
|
-
return;
|
|
5646
|
+
function maybeResolve() {
|
|
5647
|
+
if (!settled && entrysDone && pendingStreams === 0) {
|
|
5648
|
+
settleResolve({ zipfile, entries, manifestBuf, soHeaders });
|
|
5397
5649
|
}
|
|
5398
|
-
const chunks = [];
|
|
5399
|
-
let totalBytes = 0;
|
|
5400
|
-
let done = false;
|
|
5401
|
-
stream.on("data", (chunk) => {
|
|
5402
|
-
chunks.push(chunk);
|
|
5403
|
-
totalBytes += chunk.length;
|
|
5404
|
-
if (maxBytes && totalBytes >= maxBytes) {
|
|
5405
|
-
stream.destroy();
|
|
5406
|
-
}
|
|
5407
|
-
});
|
|
5408
|
-
stream.on("error", (e) => {
|
|
5409
|
-
if (maxBytes && totalBytes >= maxBytes) return;
|
|
5410
|
-
fail(e);
|
|
5411
|
-
});
|
|
5412
|
-
const finish = () => {
|
|
5413
|
-
if (done) return;
|
|
5414
|
-
done = true;
|
|
5415
|
-
const buf = Buffer.concat(chunks);
|
|
5416
|
-
onData(maxBytes ? buf.subarray(0, maxBytes) : buf);
|
|
5417
|
-
pendingStreams--;
|
|
5418
|
-
maybeResolve();
|
|
5419
|
-
};
|
|
5420
|
-
stream.on("end", finish);
|
|
5421
|
-
stream.on("close", () => {
|
|
5422
|
-
if (totalBytes > 0 && maxBytes) finish();
|
|
5423
|
-
});
|
|
5424
|
-
});
|
|
5425
|
-
}
|
|
5426
|
-
zipfile.on("error", fail);
|
|
5427
|
-
zipfile.on("entry", (entry) => {
|
|
5428
|
-
if (rejected) return;
|
|
5429
|
-
const path = entry.fileName;
|
|
5430
|
-
if (!path.endsWith("/")) {
|
|
5431
|
-
entries.push({
|
|
5432
|
-
path,
|
|
5433
|
-
compressedSize: entry.compressedSize,
|
|
5434
|
-
uncompressedSize: entry.uncompressedSize
|
|
5435
|
-
});
|
|
5436
5650
|
}
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5651
|
+
function readEntryStream(entry, onData, maxBytes) {
|
|
5652
|
+
pendingStreams++;
|
|
5653
|
+
zipfile.openReadStream(entry, (streamErr, stream) => {
|
|
5654
|
+
if (streamErr || !stream) {
|
|
5655
|
+
fail(streamErr ?? new Error(`Failed to read ${entry.fileName}`));
|
|
5656
|
+
return;
|
|
5657
|
+
}
|
|
5658
|
+
const chunks = [];
|
|
5659
|
+
let totalBytes = 0;
|
|
5660
|
+
let done = false;
|
|
5661
|
+
stream.on("data", (chunk) => {
|
|
5662
|
+
chunks.push(chunk);
|
|
5663
|
+
totalBytes += chunk.length;
|
|
5664
|
+
if (maxBytes && totalBytes >= maxBytes) {
|
|
5665
|
+
stream.destroy();
|
|
5666
|
+
}
|
|
5667
|
+
});
|
|
5668
|
+
stream.on("error", (e) => {
|
|
5669
|
+
if (maxBytes && totalBytes >= maxBytes) return;
|
|
5670
|
+
fail(e);
|
|
5671
|
+
});
|
|
5672
|
+
const finish = () => {
|
|
5673
|
+
if (done) return;
|
|
5674
|
+
done = true;
|
|
5675
|
+
const buf = Buffer.concat(chunks);
|
|
5676
|
+
onData(maxBytes ? buf.subarray(0, maxBytes) : buf);
|
|
5677
|
+
pendingStreams--;
|
|
5678
|
+
maybeResolve();
|
|
5679
|
+
};
|
|
5680
|
+
stream.on("end", finish);
|
|
5681
|
+
stream.on("close", () => {
|
|
5682
|
+
if (totalBytes > 0 && maxBytes) finish();
|
|
5683
|
+
});
|
|
5440
5684
|
});
|
|
5441
|
-
} else if (SO_PATH_RE.test(path)) {
|
|
5442
|
-
readEntryStream(
|
|
5443
|
-
entry,
|
|
5444
|
-
(buf) => {
|
|
5445
|
-
soHeaders.set(path, buf);
|
|
5446
|
-
},
|
|
5447
|
-
SO_HEADER_BYTES
|
|
5448
|
-
);
|
|
5449
5685
|
}
|
|
5686
|
+
zipfile.on("error", fail);
|
|
5687
|
+
zipfile.on("entry", (entry) => {
|
|
5688
|
+
if (settled) return;
|
|
5689
|
+
try {
|
|
5690
|
+
const path = entry.fileName;
|
|
5691
|
+
if (!path.endsWith("/")) {
|
|
5692
|
+
entries.push({
|
|
5693
|
+
path,
|
|
5694
|
+
compressedSize: entry.compressedSize,
|
|
5695
|
+
uncompressedSize: entry.uncompressedSize
|
|
5696
|
+
});
|
|
5697
|
+
}
|
|
5698
|
+
if (path === manifestPath) {
|
|
5699
|
+
readEntryStream(entry, (buf) => {
|
|
5700
|
+
manifestBuf = buf;
|
|
5701
|
+
});
|
|
5702
|
+
} else if (SO_PATH_RE.test(path)) {
|
|
5703
|
+
readEntryStream(
|
|
5704
|
+
entry,
|
|
5705
|
+
(buf) => {
|
|
5706
|
+
soHeaders.set(path, buf);
|
|
5707
|
+
},
|
|
5708
|
+
SO_HEADER_BYTES
|
|
5709
|
+
);
|
|
5710
|
+
}
|
|
5711
|
+
zipfile.readEntry();
|
|
5712
|
+
} catch (e) {
|
|
5713
|
+
fail(e instanceof Error ? e : new Error(String(e)));
|
|
5714
|
+
}
|
|
5715
|
+
});
|
|
5716
|
+
zipfile.on("end", () => {
|
|
5717
|
+
entrysDone = true;
|
|
5718
|
+
maybeResolve();
|
|
5719
|
+
});
|
|
5450
5720
|
zipfile.readEntry();
|
|
5451
5721
|
});
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
});
|
|
5456
|
-
zipfile.readEntry();
|
|
5457
|
-
});
|
|
5722
|
+
} catch (err) {
|
|
5723
|
+
settleReject(err instanceof Error ? err : new Error(String(err)));
|
|
5724
|
+
}
|
|
5458
5725
|
});
|
|
5459
5726
|
}
|
|
5460
5727
|
|
|
@@ -6018,8 +6285,8 @@ ${fileList}` + (hasPageSizeCompat ? "\nandroid:pageSizeCompat is set, so the app
|
|
|
6018
6285
|
}
|
|
6019
6286
|
|
|
6020
6287
|
// src/preflight/scanners/metadata-scanner.ts
|
|
6021
|
-
import { readdir as
|
|
6022
|
-
import { join as
|
|
6288
|
+
import { readdir as readdir5, stat as stat6, readFile as readFile12 } from "fs/promises";
|
|
6289
|
+
import { join as join9 } from "path";
|
|
6023
6290
|
var SAFE_LANG = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/;
|
|
6024
6291
|
var FILE_MAP2 = {
|
|
6025
6292
|
"title.txt": "title",
|
|
@@ -6045,7 +6312,7 @@ var metadataScanner = {
|
|
|
6045
6312
|
const findings = [];
|
|
6046
6313
|
let entries;
|
|
6047
6314
|
try {
|
|
6048
|
-
entries = await
|
|
6315
|
+
entries = await readdir5(dir);
|
|
6049
6316
|
} catch {
|
|
6050
6317
|
findings.push({
|
|
6051
6318
|
scanner: "metadata",
|
|
@@ -6070,14 +6337,14 @@ var metadataScanner = {
|
|
|
6070
6337
|
return findings;
|
|
6071
6338
|
}
|
|
6072
6339
|
for (const lang of locales) {
|
|
6073
|
-
const langDir =
|
|
6340
|
+
const langDir = join9(dir, lang);
|
|
6074
6341
|
const langStat = await stat6(langDir).catch(() => null);
|
|
6075
6342
|
if (!langStat?.isDirectory()) continue;
|
|
6076
6343
|
const fields = {};
|
|
6077
6344
|
for (const [fileName, field] of Object.entries(FILE_MAP2)) {
|
|
6078
|
-
const filePath =
|
|
6345
|
+
const filePath = join9(langDir, fileName);
|
|
6079
6346
|
try {
|
|
6080
|
-
const content = await
|
|
6347
|
+
const content = await readFile12(filePath, "utf-8");
|
|
6081
6348
|
fields[field] = content.trimEnd();
|
|
6082
6349
|
} catch {
|
|
6083
6350
|
}
|
|
@@ -6116,9 +6383,9 @@ var metadataScanner = {
|
|
|
6116
6383
|
let totalScreenshots = 0;
|
|
6117
6384
|
let phoneScreenshots = 0;
|
|
6118
6385
|
for (const ssDir of SCREENSHOT_DIRS) {
|
|
6119
|
-
const ssPath =
|
|
6386
|
+
const ssPath = join9(langDir, "images", ssDir);
|
|
6120
6387
|
try {
|
|
6121
|
-
const ssEntries = await
|
|
6388
|
+
const ssEntries = await readdir5(ssPath);
|
|
6122
6389
|
const imageFiles = ssEntries.filter((f) => /\.(png|jpe?g|webp)$/i.test(f));
|
|
6123
6390
|
totalScreenshots += imageFiles.length;
|
|
6124
6391
|
if (ssDir === "phoneScreenshots") {
|
|
@@ -6148,9 +6415,9 @@ var metadataScanner = {
|
|
|
6148
6415
|
}
|
|
6149
6416
|
}
|
|
6150
6417
|
const defaultLang = locales.includes("en-US") ? "en-US" : locales[0];
|
|
6151
|
-
const privacyPath =
|
|
6418
|
+
const privacyPath = join9(dir, defaultLang, "privacy_policy_url.txt");
|
|
6152
6419
|
try {
|
|
6153
|
-
const url = await
|
|
6420
|
+
const url = await readFile12(privacyPath, "utf-8");
|
|
6154
6421
|
if (!url.trim()) throw new Error("empty");
|
|
6155
6422
|
} catch {
|
|
6156
6423
|
findings.push({
|
|
@@ -6175,11 +6442,11 @@ var metadataScanner = {
|
|
|
6175
6442
|
};
|
|
6176
6443
|
|
|
6177
6444
|
// src/preflight/scanners/secrets-scanner.ts
|
|
6178
|
-
import { readFile as
|
|
6445
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
6179
6446
|
|
|
6180
6447
|
// src/preflight/scan-files.ts
|
|
6181
|
-
import { readdir as
|
|
6182
|
-
import { join as
|
|
6448
|
+
import { readdir as readdir6, stat as stat7 } from "fs/promises";
|
|
6449
|
+
import { join as join10, extname as extname3 } from "path";
|
|
6183
6450
|
var DEFAULT_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
6184
6451
|
".git",
|
|
6185
6452
|
"node_modules",
|
|
@@ -6196,20 +6463,20 @@ async function collectSourceFiles(dir, extensions, skipDirs = DEFAULT_SKIP_DIRS,
|
|
|
6196
6463
|
const files = [];
|
|
6197
6464
|
let entries;
|
|
6198
6465
|
try {
|
|
6199
|
-
entries = await
|
|
6466
|
+
entries = await readdir6(dir);
|
|
6200
6467
|
} catch {
|
|
6201
6468
|
return files;
|
|
6202
6469
|
}
|
|
6203
6470
|
for (const entry of entries) {
|
|
6204
6471
|
if (skipDirs.has(entry)) continue;
|
|
6205
|
-
const fullPath =
|
|
6472
|
+
const fullPath = join10(dir, entry);
|
|
6206
6473
|
const s = await stat7(fullPath).catch(() => null);
|
|
6207
6474
|
if (!s) continue;
|
|
6208
6475
|
if (s.isDirectory()) {
|
|
6209
6476
|
const sub = await collectSourceFiles(fullPath, extensions, skipDirs, maxDepth - 1);
|
|
6210
6477
|
files.push(...sub);
|
|
6211
6478
|
} else if (s.isFile()) {
|
|
6212
|
-
const ext =
|
|
6479
|
+
const ext = extname3(entry).toLowerCase();
|
|
6213
6480
|
if (extensions.has(ext) || entry.endsWith(".gradle.kts")) {
|
|
6214
6481
|
files.push(fullPath);
|
|
6215
6482
|
}
|
|
@@ -6295,7 +6562,7 @@ var secretsScanner = {
|
|
|
6295
6562
|
for (const filePath of files) {
|
|
6296
6563
|
let content;
|
|
6297
6564
|
try {
|
|
6298
|
-
content = await
|
|
6565
|
+
content = await readFile13(filePath, "utf-8");
|
|
6299
6566
|
} catch {
|
|
6300
6567
|
continue;
|
|
6301
6568
|
}
|
|
@@ -6323,7 +6590,7 @@ var secretsScanner = {
|
|
|
6323
6590
|
};
|
|
6324
6591
|
|
|
6325
6592
|
// src/preflight/scanners/billing-scanner.ts
|
|
6326
|
-
import { readFile as
|
|
6593
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
6327
6594
|
var BILLING_PATTERNS = [
|
|
6328
6595
|
{
|
|
6329
6596
|
ruleId: "billing-stripe-sdk",
|
|
@@ -6384,7 +6651,7 @@ var billingScanner = {
|
|
|
6384
6651
|
for (const filePath of files) {
|
|
6385
6652
|
let content;
|
|
6386
6653
|
try {
|
|
6387
|
-
content = await
|
|
6654
|
+
content = await readFile14(filePath, "utf-8");
|
|
6388
6655
|
} catch {
|
|
6389
6656
|
continue;
|
|
6390
6657
|
}
|
|
@@ -6410,7 +6677,7 @@ var billingScanner = {
|
|
|
6410
6677
|
};
|
|
6411
6678
|
|
|
6412
6679
|
// src/preflight/scanners/privacy-scanner.ts
|
|
6413
|
-
import { readFile as
|
|
6680
|
+
import { readFile as readFile15 } from "fs/promises";
|
|
6414
6681
|
var TRACKING_SDKS = [
|
|
6415
6682
|
{
|
|
6416
6683
|
name: "Facebook SDK",
|
|
@@ -6450,7 +6717,7 @@ var privacyScanner = {
|
|
|
6450
6717
|
for (const filePath of files) {
|
|
6451
6718
|
let content;
|
|
6452
6719
|
try {
|
|
6453
|
-
content = await
|
|
6720
|
+
content = await readFile15(filePath, "utf-8");
|
|
6454
6721
|
} catch {
|
|
6455
6722
|
continue;
|
|
6456
6723
|
}
|
|
@@ -6591,6 +6858,15 @@ var policyScanner = {
|
|
|
6591
6858
|
policyUrl: "https://support.google.com/googleplay/android-developer/answer/9876937"
|
|
6592
6859
|
});
|
|
6593
6860
|
}
|
|
6861
|
+
findings.push({
|
|
6862
|
+
scanner: "policy",
|
|
6863
|
+
ruleId: "policy-developer-verification",
|
|
6864
|
+
severity: "info",
|
|
6865
|
+
title: "Developer verification enforcement (Sep 30, 2026)",
|
|
6866
|
+
message: "Google begins enforcing developer verification on September 30, 2026, first in Brazil, Indonesia, Singapore, and Thailand. Apps distributed in those markets need a verified developer identity and a registered package name. Apps using Play App Signing are auto-registered.",
|
|
6867
|
+
suggestion: "Run `gpc verify checklist` to review readiness. This is a distribution-market reminder, not an AAB/APK defect.",
|
|
6868
|
+
policyUrl: "https://developer.android.com/developer-verification"
|
|
6869
|
+
});
|
|
6594
6870
|
if (perms.has("android.permission.SYSTEM_ALERT_WINDOW")) {
|
|
6595
6871
|
findings.push({
|
|
6596
6872
|
scanner: "policy",
|
|
@@ -6801,57 +7077,17 @@ async function runPreflight(options) {
|
|
|
6801
7077
|
};
|
|
6802
7078
|
}
|
|
6803
7079
|
|
|
6804
|
-
// src/utils/sort.ts
|
|
6805
|
-
function getNestedValue(obj, path) {
|
|
6806
|
-
const parts = path.split(".");
|
|
6807
|
-
let current = obj;
|
|
6808
|
-
for (const part of parts) {
|
|
6809
|
-
if (current === null || current === void 0 || typeof current !== "object") {
|
|
6810
|
-
return void 0;
|
|
6811
|
-
}
|
|
6812
|
-
current = current[part];
|
|
6813
|
-
}
|
|
6814
|
-
return current;
|
|
6815
|
-
}
|
|
6816
|
-
function compare(a, b) {
|
|
6817
|
-
if (a === void 0 && b === void 0) return 0;
|
|
6818
|
-
if (a === void 0) return 1;
|
|
6819
|
-
if (b === void 0) return -1;
|
|
6820
|
-
if (typeof a === "number" && typeof b === "number") {
|
|
6821
|
-
return a - b;
|
|
6822
|
-
}
|
|
6823
|
-
return String(a).localeCompare(String(b));
|
|
6824
|
-
}
|
|
6825
|
-
function sortResults(items, sortSpec) {
|
|
6826
|
-
if (!sortSpec || items.length === 0) {
|
|
6827
|
-
return items;
|
|
6828
|
-
}
|
|
6829
|
-
const descending = sortSpec.startsWith("-");
|
|
6830
|
-
const field = descending ? sortSpec.slice(1) : sortSpec;
|
|
6831
|
-
const hasField = items.some((item) => getNestedValue(item, field) !== void 0);
|
|
6832
|
-
if (!hasField) {
|
|
6833
|
-
return items;
|
|
6834
|
-
}
|
|
6835
|
-
const sorted = [...items].sort((a, b) => {
|
|
6836
|
-
const aVal = getNestedValue(a, field);
|
|
6837
|
-
const bVal = getNestedValue(b, field);
|
|
6838
|
-
const result = compare(aVal, bVal);
|
|
6839
|
-
return descending ? -result : result;
|
|
6840
|
-
});
|
|
6841
|
-
return sorted;
|
|
6842
|
-
}
|
|
6843
|
-
|
|
6844
7080
|
// src/commands/plugin-scaffold.ts
|
|
6845
|
-
import { mkdir as
|
|
6846
|
-
import { join as
|
|
7081
|
+
import { mkdir as mkdir8, writeFile as writeFile8 } from "fs/promises";
|
|
7082
|
+
import { join as join11 } from "path";
|
|
6847
7083
|
async function scaffoldPlugin(options) {
|
|
6848
7084
|
const { name, dir, description = `GPC plugin: ${name}` } = options;
|
|
6849
7085
|
const pluginName = name.startsWith("gpc-plugin-") ? name : `gpc-plugin-${name}`;
|
|
6850
7086
|
const shortName = pluginName.replace(/^gpc-plugin-/, "");
|
|
6851
|
-
const srcDir =
|
|
6852
|
-
const testDir =
|
|
6853
|
-
await
|
|
6854
|
-
await
|
|
7087
|
+
const srcDir = join11(dir, "src");
|
|
7088
|
+
const testDir = join11(dir, "tests");
|
|
7089
|
+
await mkdir8(srcDir, { recursive: true });
|
|
7090
|
+
await mkdir8(testDir, { recursive: true });
|
|
6855
7091
|
const files = [];
|
|
6856
7092
|
const pkg = {
|
|
6857
7093
|
name: pluginName,
|
|
@@ -6885,7 +7121,7 @@ async function scaffoldPlugin(options) {
|
|
|
6885
7121
|
vitest: "^3.0.0"
|
|
6886
7122
|
}
|
|
6887
7123
|
};
|
|
6888
|
-
await
|
|
7124
|
+
await writeFile8(join11(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
|
|
6889
7125
|
files.push("package.json");
|
|
6890
7126
|
const tsconfig = {
|
|
6891
7127
|
compilerOptions: {
|
|
@@ -6901,7 +7137,7 @@ async function scaffoldPlugin(options) {
|
|
|
6901
7137
|
},
|
|
6902
7138
|
include: ["src"]
|
|
6903
7139
|
};
|
|
6904
|
-
await
|
|
7140
|
+
await writeFile8(join11(dir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2) + "\n");
|
|
6905
7141
|
files.push("tsconfig.json");
|
|
6906
7142
|
const srcContent = `import { definePlugin } from "@gpc-cli/plugin-sdk";
|
|
6907
7143
|
import type { CommandEvent, CommandResult } from "@gpc-cli/plugin-sdk";
|
|
@@ -6934,7 +7170,7 @@ export const plugin = definePlugin({
|
|
|
6934
7170
|
},
|
|
6935
7171
|
});
|
|
6936
7172
|
`;
|
|
6937
|
-
await
|
|
7173
|
+
await writeFile8(join11(srcDir, "index.ts"), srcContent);
|
|
6938
7174
|
files.push("src/index.ts");
|
|
6939
7175
|
const testContent = `import { describe, it, expect, vi } from "vitest";
|
|
6940
7176
|
import { plugin } from "../src/index";
|
|
@@ -6959,7 +7195,7 @@ describe("${pluginName}", () => {
|
|
|
6959
7195
|
});
|
|
6960
7196
|
});
|
|
6961
7197
|
`;
|
|
6962
|
-
await
|
|
7198
|
+
await writeFile8(join11(testDir, "plugin.test.ts"), testContent);
|
|
6963
7199
|
files.push("tests/plugin.test.ts");
|
|
6964
7200
|
return { dir, files };
|
|
6965
7201
|
}
|
|
@@ -7075,7 +7311,7 @@ async function sendWebhook(config, payload, target) {
|
|
|
7075
7311
|
}
|
|
7076
7312
|
|
|
7077
7313
|
// src/commands/internal-sharing.ts
|
|
7078
|
-
import { extname as
|
|
7314
|
+
import { extname as extname4 } from "path";
|
|
7079
7315
|
async function uploadInternalSharing(client, packageName, filePath, fileType) {
|
|
7080
7316
|
const resolvedType = fileType ?? detectFileType(filePath);
|
|
7081
7317
|
const validation = await validateUploadFile(filePath);
|
|
@@ -7102,7 +7338,7 @@ ${validation.errors.join("\n")}`,
|
|
|
7102
7338
|
};
|
|
7103
7339
|
}
|
|
7104
7340
|
function detectFileType(filePath) {
|
|
7105
|
-
const ext =
|
|
7341
|
+
const ext = extname4(filePath).toLowerCase();
|
|
7106
7342
|
if (ext === ".aab") return "bundle";
|
|
7107
7343
|
if (ext === ".apk") return "apk";
|
|
7108
7344
|
throw new GpcError(
|
|
@@ -7114,7 +7350,7 @@ function detectFileType(filePath) {
|
|
|
7114
7350
|
}
|
|
7115
7351
|
|
|
7116
7352
|
// src/commands/generated-apks.ts
|
|
7117
|
-
import { writeFile as
|
|
7353
|
+
import { writeFile as writeFile9 } from "fs/promises";
|
|
7118
7354
|
async function listGeneratedApks(client, packageName, versionCode) {
|
|
7119
7355
|
if (!Number.isInteger(versionCode) || versionCode <= 0) {
|
|
7120
7356
|
throw new GpcError(
|
|
@@ -7145,12 +7381,12 @@ async function downloadGeneratedApk(client, packageName, versionCode, apkId, out
|
|
|
7145
7381
|
}
|
|
7146
7382
|
const buffer = await client.generatedApks.download(packageName, versionCode, apkId);
|
|
7147
7383
|
const bytes = new Uint8Array(buffer);
|
|
7148
|
-
await
|
|
7384
|
+
await writeFile9(outputPath, bytes);
|
|
7149
7385
|
return { path: outputPath, sizeBytes: bytes.byteLength };
|
|
7150
7386
|
}
|
|
7151
7387
|
|
|
7152
7388
|
// src/commands/bundle-analysis.ts
|
|
7153
|
-
import { readFile as
|
|
7389
|
+
import { readFile as readFile16, stat as stat8 } from "fs/promises";
|
|
7154
7390
|
var EOCD_SIGNATURE = 101010256;
|
|
7155
7391
|
var CD_SIGNATURE = 33639248;
|
|
7156
7392
|
var MODULE_SUBDIRS = /* @__PURE__ */ new Set(["dex", "manifest", "res", "assets", "lib", "resources.pb", "root"]);
|
|
@@ -7230,7 +7466,7 @@ async function analyzeBundle(filePath) {
|
|
|
7230
7466
|
if (!fileInfo || !fileInfo.isFile()) {
|
|
7231
7467
|
throw new Error(`File not found: ${filePath}`);
|
|
7232
7468
|
}
|
|
7233
|
-
const buf = await
|
|
7469
|
+
const buf = await readFile16(filePath);
|
|
7234
7470
|
const cdEntries = parseCentralDirectory(buf);
|
|
7235
7471
|
const fileType = detectFileType2(filePath);
|
|
7236
7472
|
const isAab = fileType === "aab";
|
|
@@ -7316,7 +7552,7 @@ function topFiles(analysis, n = 20) {
|
|
|
7316
7552
|
async function checkBundleSize(analysis, configPath = ".bundlesize.json") {
|
|
7317
7553
|
let config;
|
|
7318
7554
|
try {
|
|
7319
|
-
const raw = await
|
|
7555
|
+
const raw = await readFile16(configPath, "utf-8");
|
|
7320
7556
|
config = JSON.parse(raw);
|
|
7321
7557
|
} catch {
|
|
7322
7558
|
return { passed: true, violations: [] };
|
|
@@ -8677,8 +8913,8 @@ async function sha256File(filePath) {
|
|
|
8677
8913
|
}
|
|
8678
8914
|
|
|
8679
8915
|
// src/commands/image-sync.ts
|
|
8680
|
-
import { readdir as
|
|
8681
|
-
import { join as
|
|
8916
|
+
import { readdir as readdir7 } from "fs/promises";
|
|
8917
|
+
import { join as join12, extname as extname5 } from "path";
|
|
8682
8918
|
import { PlayApiError } from "@gpc-cli/api";
|
|
8683
8919
|
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg"]);
|
|
8684
8920
|
var ALL_IMAGE_TYPES2 = [
|
|
@@ -8693,15 +8929,15 @@ var ALL_IMAGE_TYPES2 = [
|
|
|
8693
8929
|
];
|
|
8694
8930
|
async function scanLocalImages(dir) {
|
|
8695
8931
|
try {
|
|
8696
|
-
const entries = await
|
|
8697
|
-
return entries.filter((e) => e.isFile() && IMAGE_EXTENSIONS.has(
|
|
8932
|
+
const entries = await readdir7(dir, { withFileTypes: true });
|
|
8933
|
+
return entries.filter((e) => e.isFile() && IMAGE_EXTENSIONS.has(extname5(e.name).toLowerCase())).map((e) => e.name).sort();
|
|
8698
8934
|
} catch {
|
|
8699
8935
|
return [];
|
|
8700
8936
|
}
|
|
8701
8937
|
}
|
|
8702
8938
|
async function scanLanguages(dir) {
|
|
8703
8939
|
try {
|
|
8704
|
-
const entries = await
|
|
8940
|
+
const entries = await readdir7(dir, { withFileTypes: true });
|
|
8705
8941
|
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
8706
8942
|
} catch {
|
|
8707
8943
|
return [];
|
|
@@ -8726,7 +8962,7 @@ async function syncImages(client, packageName, dir, options) {
|
|
|
8726
8962
|
try {
|
|
8727
8963
|
for (const language of languages) {
|
|
8728
8964
|
for (const imageType of imageTypes) {
|
|
8729
|
-
const localDir =
|
|
8965
|
+
const localDir = join12(dir, language, imageType);
|
|
8730
8966
|
const localFiles = await scanLocalImages(localDir);
|
|
8731
8967
|
let remoteImages;
|
|
8732
8968
|
try {
|
|
@@ -8741,7 +8977,7 @@ async function syncImages(client, packageName, dir, options) {
|
|
|
8741
8977
|
const remoteSha256Set = new Set(remoteImages.map((img) => img.sha256.toLowerCase()));
|
|
8742
8978
|
const localHashes = /* @__PURE__ */ new Map();
|
|
8743
8979
|
for (const file of localFiles) {
|
|
8744
|
-
const hash = await sha256File(
|
|
8980
|
+
const hash = await sha256File(join12(localDir, file));
|
|
8745
8981
|
localHashes.set(file, hash);
|
|
8746
8982
|
}
|
|
8747
8983
|
const localSha256Set = new Set(localHashes.values());
|
|
@@ -8769,7 +9005,7 @@ async function syncImages(client, packageName, dir, options) {
|
|
|
8769
9005
|
details.push({ language, imageType, file, action: "skip", reason: "sha256 match" });
|
|
8770
9006
|
} else {
|
|
8771
9007
|
if (!options?.dryRun) {
|
|
8772
|
-
const filePath =
|
|
9008
|
+
const filePath = join12(localDir, file);
|
|
8773
9009
|
const check = await validateImage(filePath, imageType);
|
|
8774
9010
|
if (!check.valid) {
|
|
8775
9011
|
throw new GpcError(
|
|
@@ -8928,6 +9164,15 @@ function buildChecklist(input) {
|
|
|
8928
9164
|
answers
|
|
8929
9165
|
)
|
|
8930
9166
|
);
|
|
9167
|
+
items.push(
|
|
9168
|
+
resolveManualStep(
|
|
9169
|
+
"enforcement-markets",
|
|
9170
|
+
"Verification enforcement timeline reviewed",
|
|
9171
|
+
"Enforcement begins September 30, 2026, first in Brazil, Indonesia, Singapore, and Thailand. Apps distributed there need a verified developer identity and a registered package name. Apps using Play App Signing are auto-registered.",
|
|
9172
|
+
VERIFICATION_PAGE,
|
|
9173
|
+
answers
|
|
9174
|
+
)
|
|
9175
|
+
);
|
|
8931
9176
|
items.push(
|
|
8932
9177
|
resolveManualStep(
|
|
8933
9178
|
"auto-registration-reviewed",
|
|
@@ -9004,6 +9249,77 @@ function renderChecklistMarkdown(result, accountEmail) {
|
|
|
9004
9249
|
lines.push("");
|
|
9005
9250
|
return lines.join("\n");
|
|
9006
9251
|
}
|
|
9252
|
+
|
|
9253
|
+
// src/commands/readiness.ts
|
|
9254
|
+
var STATUS_FRACTION = {
|
|
9255
|
+
pass: 1,
|
|
9256
|
+
warn: 0.5,
|
|
9257
|
+
fail: 0
|
|
9258
|
+
};
|
|
9259
|
+
function gradeFromPercent(percent) {
|
|
9260
|
+
if (percent >= 90) return "A";
|
|
9261
|
+
if (percent >= 80) return "B";
|
|
9262
|
+
if (percent >= 70) return "C";
|
|
9263
|
+
if (percent >= 60) return "D";
|
|
9264
|
+
return "F";
|
|
9265
|
+
}
|
|
9266
|
+
function scoreReadiness(signals) {
|
|
9267
|
+
let earned = 0;
|
|
9268
|
+
let possible = 0;
|
|
9269
|
+
const breakdown = [];
|
|
9270
|
+
const suggestions = [];
|
|
9271
|
+
for (const signal of signals) {
|
|
9272
|
+
if (signal.status === "na") {
|
|
9273
|
+
breakdown.push({
|
|
9274
|
+
key: signal.key,
|
|
9275
|
+
label: signal.label,
|
|
9276
|
+
weight: signal.weight,
|
|
9277
|
+
status: "na",
|
|
9278
|
+
points: 0,
|
|
9279
|
+
detail: signal.detail
|
|
9280
|
+
});
|
|
9281
|
+
continue;
|
|
9282
|
+
}
|
|
9283
|
+
const points = signal.weight * STATUS_FRACTION[signal.status];
|
|
9284
|
+
earned += points;
|
|
9285
|
+
possible += signal.weight;
|
|
9286
|
+
breakdown.push({
|
|
9287
|
+
key: signal.key,
|
|
9288
|
+
label: signal.label,
|
|
9289
|
+
weight: signal.weight,
|
|
9290
|
+
status: signal.status,
|
|
9291
|
+
points,
|
|
9292
|
+
detail: signal.detail
|
|
9293
|
+
});
|
|
9294
|
+
if (signal.status !== "pass" && signal.suggestion) {
|
|
9295
|
+
suggestions.push(signal.suggestion);
|
|
9296
|
+
}
|
|
9297
|
+
}
|
|
9298
|
+
const evaluated = possible > 0;
|
|
9299
|
+
const percent = evaluated ? Math.round(earned / possible * 100) : 0;
|
|
9300
|
+
const grade = evaluated ? gradeFromPercent(percent) : "F";
|
|
9301
|
+
return { grade, percent, evaluated, earned, possible, breakdown, suggestions };
|
|
9302
|
+
}
|
|
9303
|
+
var GRADE_COLOR = {
|
|
9304
|
+
A: "brightgreen",
|
|
9305
|
+
B: "green",
|
|
9306
|
+
C: "yellow",
|
|
9307
|
+
D: "orange",
|
|
9308
|
+
F: "red"
|
|
9309
|
+
};
|
|
9310
|
+
function readinessBadgeColor(grade) {
|
|
9311
|
+
return GRADE_COLOR[grade];
|
|
9312
|
+
}
|
|
9313
|
+
function readinessBadgeUrl(score) {
|
|
9314
|
+
const label = "GPC readiness";
|
|
9315
|
+
const message = score.evaluated ? `${score.grade} (${score.percent}%)` : "not evaluated";
|
|
9316
|
+
const color = score.evaluated ? readinessBadgeColor(score.grade) : "lightgrey";
|
|
9317
|
+
const enc = (segment) => encodeURIComponent(segment.replace(/-/g, "--").replace(/_/g, "__"));
|
|
9318
|
+
return `https://img.shields.io/badge/${enc(label)}-${enc(message)}-${color}`;
|
|
9319
|
+
}
|
|
9320
|
+
function readinessBadgeMarkdown(score) {
|
|
9321
|
+
return `})`;
|
|
9322
|
+
}
|
|
9007
9323
|
export {
|
|
9008
9324
|
ApiError,
|
|
9009
9325
|
ConfigError,
|
|
@@ -9020,6 +9336,7 @@ export {
|
|
|
9020
9336
|
PROVIDER_WHITELIST,
|
|
9021
9337
|
PluginManager,
|
|
9022
9338
|
RENDERERS,
|
|
9339
|
+
REVIEW_SORT_PRESETS,
|
|
9023
9340
|
SECTION_ORDER,
|
|
9024
9341
|
SENSITIVE_ARG_KEYS,
|
|
9025
9342
|
SENSITIVE_KEYS,
|
|
@@ -9166,6 +9483,7 @@ export {
|
|
|
9166
9483
|
getVitalsOverview,
|
|
9167
9484
|
getVitalsRendering,
|
|
9168
9485
|
getVitalsStartup,
|
|
9486
|
+
gradeFromPercent,
|
|
9169
9487
|
handleBreach,
|
|
9170
9488
|
importDataSafety,
|
|
9171
9489
|
importTestersFromCsv,
|
|
@@ -9220,11 +9538,18 @@ export {
|
|
|
9220
9538
|
promoteRelease,
|
|
9221
9539
|
publish,
|
|
9222
9540
|
publishEnterpriseApp,
|
|
9541
|
+
pullAchievementConfigs,
|
|
9542
|
+
pullLeaderboardConfigs,
|
|
9223
9543
|
pullListings,
|
|
9544
|
+
pushAchievementConfigs,
|
|
9545
|
+
pushLeaderboardConfigs,
|
|
9224
9546
|
pushListings,
|
|
9225
9547
|
readListingsFromDir,
|
|
9226
9548
|
readReleaseNotesForVersion,
|
|
9227
9549
|
readReleaseNotesFromDir,
|
|
9550
|
+
readinessBadgeColor,
|
|
9551
|
+
readinessBadgeMarkdown,
|
|
9552
|
+
readinessBadgeUrl,
|
|
9228
9553
|
redactAuditArgs,
|
|
9229
9554
|
redactSensitive,
|
|
9230
9555
|
refundExternalTransaction,
|
|
@@ -9252,12 +9577,16 @@ export {
|
|
|
9252
9577
|
safePathWithin,
|
|
9253
9578
|
saveStatusCache,
|
|
9254
9579
|
scaffoldPlugin,
|
|
9580
|
+
scoreReadiness,
|
|
9255
9581
|
searchAuditEvents,
|
|
9256
9582
|
searchVitalsErrors,
|
|
9257
9583
|
sendNotification,
|
|
9258
9584
|
sendWebhook,
|
|
9585
|
+
setAchievementIcon,
|
|
9586
|
+
setLeaderboardIcon,
|
|
9259
9587
|
sha256File,
|
|
9260
9588
|
sortResults,
|
|
9589
|
+
sortReviews,
|
|
9261
9590
|
startTrain,
|
|
9262
9591
|
statusHasBreach,
|
|
9263
9592
|
syncImages,
|