@gpc-cli/core 0.9.74 → 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 +417 -124
- 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,7 @@ async function loadPreflightConfig(configPath) {
|
|
|
5132
5363
|
}
|
|
5133
5364
|
|
|
5134
5365
|
// src/preflight/aab-reader.ts
|
|
5135
|
-
import { readFile as
|
|
5366
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
5136
5367
|
import { fromBuffer } from "yauzl";
|
|
5137
5368
|
|
|
5138
5369
|
// src/preflight/manifest-parser.ts
|
|
@@ -5330,10 +5561,10 @@ async function readAab(aabPath) {
|
|
|
5330
5561
|
const manifestPath = detectManifestPath(aabPath);
|
|
5331
5562
|
let buffer;
|
|
5332
5563
|
try {
|
|
5333
|
-
buffer = await
|
|
5564
|
+
buffer = await readFile11(aabPath);
|
|
5334
5565
|
} catch (err) {
|
|
5335
5566
|
const msg = err instanceof Error ? err.message : String(err);
|
|
5336
|
-
throw new Error(`Could not read ${aabPath}: ${msg}
|
|
5567
|
+
throw new Error(`Could not read ${aabPath}: ${msg}`, { cause: err });
|
|
5337
5568
|
}
|
|
5338
5569
|
const { zipfile, entries, manifestBuf, soHeaders } = await openAndScan(buffer, manifestPath);
|
|
5339
5570
|
zipfile.close();
|
|
@@ -6054,8 +6285,8 @@ ${fileList}` + (hasPageSizeCompat ? "\nandroid:pageSizeCompat is set, so the app
|
|
|
6054
6285
|
}
|
|
6055
6286
|
|
|
6056
6287
|
// src/preflight/scanners/metadata-scanner.ts
|
|
6057
|
-
import { readdir as
|
|
6058
|
-
import { join as
|
|
6288
|
+
import { readdir as readdir5, stat as stat6, readFile as readFile12 } from "fs/promises";
|
|
6289
|
+
import { join as join9 } from "path";
|
|
6059
6290
|
var SAFE_LANG = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/;
|
|
6060
6291
|
var FILE_MAP2 = {
|
|
6061
6292
|
"title.txt": "title",
|
|
@@ -6081,7 +6312,7 @@ var metadataScanner = {
|
|
|
6081
6312
|
const findings = [];
|
|
6082
6313
|
let entries;
|
|
6083
6314
|
try {
|
|
6084
|
-
entries = await
|
|
6315
|
+
entries = await readdir5(dir);
|
|
6085
6316
|
} catch {
|
|
6086
6317
|
findings.push({
|
|
6087
6318
|
scanner: "metadata",
|
|
@@ -6106,14 +6337,14 @@ var metadataScanner = {
|
|
|
6106
6337
|
return findings;
|
|
6107
6338
|
}
|
|
6108
6339
|
for (const lang of locales) {
|
|
6109
|
-
const langDir =
|
|
6340
|
+
const langDir = join9(dir, lang);
|
|
6110
6341
|
const langStat = await stat6(langDir).catch(() => null);
|
|
6111
6342
|
if (!langStat?.isDirectory()) continue;
|
|
6112
6343
|
const fields = {};
|
|
6113
6344
|
for (const [fileName, field] of Object.entries(FILE_MAP2)) {
|
|
6114
|
-
const filePath =
|
|
6345
|
+
const filePath = join9(langDir, fileName);
|
|
6115
6346
|
try {
|
|
6116
|
-
const content = await
|
|
6347
|
+
const content = await readFile12(filePath, "utf-8");
|
|
6117
6348
|
fields[field] = content.trimEnd();
|
|
6118
6349
|
} catch {
|
|
6119
6350
|
}
|
|
@@ -6152,9 +6383,9 @@ var metadataScanner = {
|
|
|
6152
6383
|
let totalScreenshots = 0;
|
|
6153
6384
|
let phoneScreenshots = 0;
|
|
6154
6385
|
for (const ssDir of SCREENSHOT_DIRS) {
|
|
6155
|
-
const ssPath =
|
|
6386
|
+
const ssPath = join9(langDir, "images", ssDir);
|
|
6156
6387
|
try {
|
|
6157
|
-
const ssEntries = await
|
|
6388
|
+
const ssEntries = await readdir5(ssPath);
|
|
6158
6389
|
const imageFiles = ssEntries.filter((f) => /\.(png|jpe?g|webp)$/i.test(f));
|
|
6159
6390
|
totalScreenshots += imageFiles.length;
|
|
6160
6391
|
if (ssDir === "phoneScreenshots") {
|
|
@@ -6184,9 +6415,9 @@ var metadataScanner = {
|
|
|
6184
6415
|
}
|
|
6185
6416
|
}
|
|
6186
6417
|
const defaultLang = locales.includes("en-US") ? "en-US" : locales[0];
|
|
6187
|
-
const privacyPath =
|
|
6418
|
+
const privacyPath = join9(dir, defaultLang, "privacy_policy_url.txt");
|
|
6188
6419
|
try {
|
|
6189
|
-
const url = await
|
|
6420
|
+
const url = await readFile12(privacyPath, "utf-8");
|
|
6190
6421
|
if (!url.trim()) throw new Error("empty");
|
|
6191
6422
|
} catch {
|
|
6192
6423
|
findings.push({
|
|
@@ -6211,11 +6442,11 @@ var metadataScanner = {
|
|
|
6211
6442
|
};
|
|
6212
6443
|
|
|
6213
6444
|
// src/preflight/scanners/secrets-scanner.ts
|
|
6214
|
-
import { readFile as
|
|
6445
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
6215
6446
|
|
|
6216
6447
|
// src/preflight/scan-files.ts
|
|
6217
|
-
import { readdir as
|
|
6218
|
-
import { join as
|
|
6448
|
+
import { readdir as readdir6, stat as stat7 } from "fs/promises";
|
|
6449
|
+
import { join as join10, extname as extname3 } from "path";
|
|
6219
6450
|
var DEFAULT_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
6220
6451
|
".git",
|
|
6221
6452
|
"node_modules",
|
|
@@ -6232,20 +6463,20 @@ async function collectSourceFiles(dir, extensions, skipDirs = DEFAULT_SKIP_DIRS,
|
|
|
6232
6463
|
const files = [];
|
|
6233
6464
|
let entries;
|
|
6234
6465
|
try {
|
|
6235
|
-
entries = await
|
|
6466
|
+
entries = await readdir6(dir);
|
|
6236
6467
|
} catch {
|
|
6237
6468
|
return files;
|
|
6238
6469
|
}
|
|
6239
6470
|
for (const entry of entries) {
|
|
6240
6471
|
if (skipDirs.has(entry)) continue;
|
|
6241
|
-
const fullPath =
|
|
6472
|
+
const fullPath = join10(dir, entry);
|
|
6242
6473
|
const s = await stat7(fullPath).catch(() => null);
|
|
6243
6474
|
if (!s) continue;
|
|
6244
6475
|
if (s.isDirectory()) {
|
|
6245
6476
|
const sub = await collectSourceFiles(fullPath, extensions, skipDirs, maxDepth - 1);
|
|
6246
6477
|
files.push(...sub);
|
|
6247
6478
|
} else if (s.isFile()) {
|
|
6248
|
-
const ext =
|
|
6479
|
+
const ext = extname3(entry).toLowerCase();
|
|
6249
6480
|
if (extensions.has(ext) || entry.endsWith(".gradle.kts")) {
|
|
6250
6481
|
files.push(fullPath);
|
|
6251
6482
|
}
|
|
@@ -6331,7 +6562,7 @@ var secretsScanner = {
|
|
|
6331
6562
|
for (const filePath of files) {
|
|
6332
6563
|
let content;
|
|
6333
6564
|
try {
|
|
6334
|
-
content = await
|
|
6565
|
+
content = await readFile13(filePath, "utf-8");
|
|
6335
6566
|
} catch {
|
|
6336
6567
|
continue;
|
|
6337
6568
|
}
|
|
@@ -6359,7 +6590,7 @@ var secretsScanner = {
|
|
|
6359
6590
|
};
|
|
6360
6591
|
|
|
6361
6592
|
// src/preflight/scanners/billing-scanner.ts
|
|
6362
|
-
import { readFile as
|
|
6593
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
6363
6594
|
var BILLING_PATTERNS = [
|
|
6364
6595
|
{
|
|
6365
6596
|
ruleId: "billing-stripe-sdk",
|
|
@@ -6420,7 +6651,7 @@ var billingScanner = {
|
|
|
6420
6651
|
for (const filePath of files) {
|
|
6421
6652
|
let content;
|
|
6422
6653
|
try {
|
|
6423
|
-
content = await
|
|
6654
|
+
content = await readFile14(filePath, "utf-8");
|
|
6424
6655
|
} catch {
|
|
6425
6656
|
continue;
|
|
6426
6657
|
}
|
|
@@ -6446,7 +6677,7 @@ var billingScanner = {
|
|
|
6446
6677
|
};
|
|
6447
6678
|
|
|
6448
6679
|
// src/preflight/scanners/privacy-scanner.ts
|
|
6449
|
-
import { readFile as
|
|
6680
|
+
import { readFile as readFile15 } from "fs/promises";
|
|
6450
6681
|
var TRACKING_SDKS = [
|
|
6451
6682
|
{
|
|
6452
6683
|
name: "Facebook SDK",
|
|
@@ -6486,7 +6717,7 @@ var privacyScanner = {
|
|
|
6486
6717
|
for (const filePath of files) {
|
|
6487
6718
|
let content;
|
|
6488
6719
|
try {
|
|
6489
|
-
content = await
|
|
6720
|
+
content = await readFile15(filePath, "utf-8");
|
|
6490
6721
|
} catch {
|
|
6491
6722
|
continue;
|
|
6492
6723
|
}
|
|
@@ -6627,6 +6858,15 @@ var policyScanner = {
|
|
|
6627
6858
|
policyUrl: "https://support.google.com/googleplay/android-developer/answer/9876937"
|
|
6628
6859
|
});
|
|
6629
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
|
+
});
|
|
6630
6870
|
if (perms.has("android.permission.SYSTEM_ALERT_WINDOW")) {
|
|
6631
6871
|
findings.push({
|
|
6632
6872
|
scanner: "policy",
|
|
@@ -6837,57 +7077,17 @@ async function runPreflight(options) {
|
|
|
6837
7077
|
};
|
|
6838
7078
|
}
|
|
6839
7079
|
|
|
6840
|
-
// src/utils/sort.ts
|
|
6841
|
-
function getNestedValue(obj, path) {
|
|
6842
|
-
const parts = path.split(".");
|
|
6843
|
-
let current = obj;
|
|
6844
|
-
for (const part of parts) {
|
|
6845
|
-
if (current === null || current === void 0 || typeof current !== "object") {
|
|
6846
|
-
return void 0;
|
|
6847
|
-
}
|
|
6848
|
-
current = current[part];
|
|
6849
|
-
}
|
|
6850
|
-
return current;
|
|
6851
|
-
}
|
|
6852
|
-
function compare(a, b) {
|
|
6853
|
-
if (a === void 0 && b === void 0) return 0;
|
|
6854
|
-
if (a === void 0) return 1;
|
|
6855
|
-
if (b === void 0) return -1;
|
|
6856
|
-
if (typeof a === "number" && typeof b === "number") {
|
|
6857
|
-
return a - b;
|
|
6858
|
-
}
|
|
6859
|
-
return String(a).localeCompare(String(b));
|
|
6860
|
-
}
|
|
6861
|
-
function sortResults(items, sortSpec) {
|
|
6862
|
-
if (!sortSpec || items.length === 0) {
|
|
6863
|
-
return items;
|
|
6864
|
-
}
|
|
6865
|
-
const descending = sortSpec.startsWith("-");
|
|
6866
|
-
const field = descending ? sortSpec.slice(1) : sortSpec;
|
|
6867
|
-
const hasField = items.some((item) => getNestedValue(item, field) !== void 0);
|
|
6868
|
-
if (!hasField) {
|
|
6869
|
-
return items;
|
|
6870
|
-
}
|
|
6871
|
-
const sorted = [...items].sort((a, b) => {
|
|
6872
|
-
const aVal = getNestedValue(a, field);
|
|
6873
|
-
const bVal = getNestedValue(b, field);
|
|
6874
|
-
const result = compare(aVal, bVal);
|
|
6875
|
-
return descending ? -result : result;
|
|
6876
|
-
});
|
|
6877
|
-
return sorted;
|
|
6878
|
-
}
|
|
6879
|
-
|
|
6880
7080
|
// src/commands/plugin-scaffold.ts
|
|
6881
|
-
import { mkdir as
|
|
6882
|
-
import { join as
|
|
7081
|
+
import { mkdir as mkdir8, writeFile as writeFile8 } from "fs/promises";
|
|
7082
|
+
import { join as join11 } from "path";
|
|
6883
7083
|
async function scaffoldPlugin(options) {
|
|
6884
7084
|
const { name, dir, description = `GPC plugin: ${name}` } = options;
|
|
6885
7085
|
const pluginName = name.startsWith("gpc-plugin-") ? name : `gpc-plugin-${name}`;
|
|
6886
7086
|
const shortName = pluginName.replace(/^gpc-plugin-/, "");
|
|
6887
|
-
const srcDir =
|
|
6888
|
-
const testDir =
|
|
6889
|
-
await
|
|
6890
|
-
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 });
|
|
6891
7091
|
const files = [];
|
|
6892
7092
|
const pkg = {
|
|
6893
7093
|
name: pluginName,
|
|
@@ -6921,7 +7121,7 @@ async function scaffoldPlugin(options) {
|
|
|
6921
7121
|
vitest: "^3.0.0"
|
|
6922
7122
|
}
|
|
6923
7123
|
};
|
|
6924
|
-
await
|
|
7124
|
+
await writeFile8(join11(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
|
|
6925
7125
|
files.push("package.json");
|
|
6926
7126
|
const tsconfig = {
|
|
6927
7127
|
compilerOptions: {
|
|
@@ -6937,7 +7137,7 @@ async function scaffoldPlugin(options) {
|
|
|
6937
7137
|
},
|
|
6938
7138
|
include: ["src"]
|
|
6939
7139
|
};
|
|
6940
|
-
await
|
|
7140
|
+
await writeFile8(join11(dir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2) + "\n");
|
|
6941
7141
|
files.push("tsconfig.json");
|
|
6942
7142
|
const srcContent = `import { definePlugin } from "@gpc-cli/plugin-sdk";
|
|
6943
7143
|
import type { CommandEvent, CommandResult } from "@gpc-cli/plugin-sdk";
|
|
@@ -6970,7 +7170,7 @@ export const plugin = definePlugin({
|
|
|
6970
7170
|
},
|
|
6971
7171
|
});
|
|
6972
7172
|
`;
|
|
6973
|
-
await
|
|
7173
|
+
await writeFile8(join11(srcDir, "index.ts"), srcContent);
|
|
6974
7174
|
files.push("src/index.ts");
|
|
6975
7175
|
const testContent = `import { describe, it, expect, vi } from "vitest";
|
|
6976
7176
|
import { plugin } from "../src/index";
|
|
@@ -6995,7 +7195,7 @@ describe("${pluginName}", () => {
|
|
|
6995
7195
|
});
|
|
6996
7196
|
});
|
|
6997
7197
|
`;
|
|
6998
|
-
await
|
|
7198
|
+
await writeFile8(join11(testDir, "plugin.test.ts"), testContent);
|
|
6999
7199
|
files.push("tests/plugin.test.ts");
|
|
7000
7200
|
return { dir, files };
|
|
7001
7201
|
}
|
|
@@ -7111,7 +7311,7 @@ async function sendWebhook(config, payload, target) {
|
|
|
7111
7311
|
}
|
|
7112
7312
|
|
|
7113
7313
|
// src/commands/internal-sharing.ts
|
|
7114
|
-
import { extname as
|
|
7314
|
+
import { extname as extname4 } from "path";
|
|
7115
7315
|
async function uploadInternalSharing(client, packageName, filePath, fileType) {
|
|
7116
7316
|
const resolvedType = fileType ?? detectFileType(filePath);
|
|
7117
7317
|
const validation = await validateUploadFile(filePath);
|
|
@@ -7138,7 +7338,7 @@ ${validation.errors.join("\n")}`,
|
|
|
7138
7338
|
};
|
|
7139
7339
|
}
|
|
7140
7340
|
function detectFileType(filePath) {
|
|
7141
|
-
const ext =
|
|
7341
|
+
const ext = extname4(filePath).toLowerCase();
|
|
7142
7342
|
if (ext === ".aab") return "bundle";
|
|
7143
7343
|
if (ext === ".apk") return "apk";
|
|
7144
7344
|
throw new GpcError(
|
|
@@ -7150,7 +7350,7 @@ function detectFileType(filePath) {
|
|
|
7150
7350
|
}
|
|
7151
7351
|
|
|
7152
7352
|
// src/commands/generated-apks.ts
|
|
7153
|
-
import { writeFile as
|
|
7353
|
+
import { writeFile as writeFile9 } from "fs/promises";
|
|
7154
7354
|
async function listGeneratedApks(client, packageName, versionCode) {
|
|
7155
7355
|
if (!Number.isInteger(versionCode) || versionCode <= 0) {
|
|
7156
7356
|
throw new GpcError(
|
|
@@ -7181,12 +7381,12 @@ async function downloadGeneratedApk(client, packageName, versionCode, apkId, out
|
|
|
7181
7381
|
}
|
|
7182
7382
|
const buffer = await client.generatedApks.download(packageName, versionCode, apkId);
|
|
7183
7383
|
const bytes = new Uint8Array(buffer);
|
|
7184
|
-
await
|
|
7384
|
+
await writeFile9(outputPath, bytes);
|
|
7185
7385
|
return { path: outputPath, sizeBytes: bytes.byteLength };
|
|
7186
7386
|
}
|
|
7187
7387
|
|
|
7188
7388
|
// src/commands/bundle-analysis.ts
|
|
7189
|
-
import { readFile as
|
|
7389
|
+
import { readFile as readFile16, stat as stat8 } from "fs/promises";
|
|
7190
7390
|
var EOCD_SIGNATURE = 101010256;
|
|
7191
7391
|
var CD_SIGNATURE = 33639248;
|
|
7192
7392
|
var MODULE_SUBDIRS = /* @__PURE__ */ new Set(["dex", "manifest", "res", "assets", "lib", "resources.pb", "root"]);
|
|
@@ -7266,7 +7466,7 @@ async function analyzeBundle(filePath) {
|
|
|
7266
7466
|
if (!fileInfo || !fileInfo.isFile()) {
|
|
7267
7467
|
throw new Error(`File not found: ${filePath}`);
|
|
7268
7468
|
}
|
|
7269
|
-
const buf = await
|
|
7469
|
+
const buf = await readFile16(filePath);
|
|
7270
7470
|
const cdEntries = parseCentralDirectory(buf);
|
|
7271
7471
|
const fileType = detectFileType2(filePath);
|
|
7272
7472
|
const isAab = fileType === "aab";
|
|
@@ -7352,7 +7552,7 @@ function topFiles(analysis, n = 20) {
|
|
|
7352
7552
|
async function checkBundleSize(analysis, configPath = ".bundlesize.json") {
|
|
7353
7553
|
let config;
|
|
7354
7554
|
try {
|
|
7355
|
-
const raw = await
|
|
7555
|
+
const raw = await readFile16(configPath, "utf-8");
|
|
7356
7556
|
config = JSON.parse(raw);
|
|
7357
7557
|
} catch {
|
|
7358
7558
|
return { passed: true, violations: [] };
|
|
@@ -8713,8 +8913,8 @@ async function sha256File(filePath) {
|
|
|
8713
8913
|
}
|
|
8714
8914
|
|
|
8715
8915
|
// src/commands/image-sync.ts
|
|
8716
|
-
import { readdir as
|
|
8717
|
-
import { join as
|
|
8916
|
+
import { readdir as readdir7 } from "fs/promises";
|
|
8917
|
+
import { join as join12, extname as extname5 } from "path";
|
|
8718
8918
|
import { PlayApiError } from "@gpc-cli/api";
|
|
8719
8919
|
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg"]);
|
|
8720
8920
|
var ALL_IMAGE_TYPES2 = [
|
|
@@ -8729,15 +8929,15 @@ var ALL_IMAGE_TYPES2 = [
|
|
|
8729
8929
|
];
|
|
8730
8930
|
async function scanLocalImages(dir) {
|
|
8731
8931
|
try {
|
|
8732
|
-
const entries = await
|
|
8733
|
-
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();
|
|
8734
8934
|
} catch {
|
|
8735
8935
|
return [];
|
|
8736
8936
|
}
|
|
8737
8937
|
}
|
|
8738
8938
|
async function scanLanguages(dir) {
|
|
8739
8939
|
try {
|
|
8740
|
-
const entries = await
|
|
8940
|
+
const entries = await readdir7(dir, { withFileTypes: true });
|
|
8741
8941
|
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
8742
8942
|
} catch {
|
|
8743
8943
|
return [];
|
|
@@ -8762,7 +8962,7 @@ async function syncImages(client, packageName, dir, options) {
|
|
|
8762
8962
|
try {
|
|
8763
8963
|
for (const language of languages) {
|
|
8764
8964
|
for (const imageType of imageTypes) {
|
|
8765
|
-
const localDir =
|
|
8965
|
+
const localDir = join12(dir, language, imageType);
|
|
8766
8966
|
const localFiles = await scanLocalImages(localDir);
|
|
8767
8967
|
let remoteImages;
|
|
8768
8968
|
try {
|
|
@@ -8777,7 +8977,7 @@ async function syncImages(client, packageName, dir, options) {
|
|
|
8777
8977
|
const remoteSha256Set = new Set(remoteImages.map((img) => img.sha256.toLowerCase()));
|
|
8778
8978
|
const localHashes = /* @__PURE__ */ new Map();
|
|
8779
8979
|
for (const file of localFiles) {
|
|
8780
|
-
const hash = await sha256File(
|
|
8980
|
+
const hash = await sha256File(join12(localDir, file));
|
|
8781
8981
|
localHashes.set(file, hash);
|
|
8782
8982
|
}
|
|
8783
8983
|
const localSha256Set = new Set(localHashes.values());
|
|
@@ -8805,7 +9005,7 @@ async function syncImages(client, packageName, dir, options) {
|
|
|
8805
9005
|
details.push({ language, imageType, file, action: "skip", reason: "sha256 match" });
|
|
8806
9006
|
} else {
|
|
8807
9007
|
if (!options?.dryRun) {
|
|
8808
|
-
const filePath =
|
|
9008
|
+
const filePath = join12(localDir, file);
|
|
8809
9009
|
const check = await validateImage(filePath, imageType);
|
|
8810
9010
|
if (!check.valid) {
|
|
8811
9011
|
throw new GpcError(
|
|
@@ -8964,6 +9164,15 @@ function buildChecklist(input) {
|
|
|
8964
9164
|
answers
|
|
8965
9165
|
)
|
|
8966
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
|
+
);
|
|
8967
9176
|
items.push(
|
|
8968
9177
|
resolveManualStep(
|
|
8969
9178
|
"auto-registration-reviewed",
|
|
@@ -9040,6 +9249,77 @@ function renderChecklistMarkdown(result, accountEmail) {
|
|
|
9040
9249
|
lines.push("");
|
|
9041
9250
|
return lines.join("\n");
|
|
9042
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
|
+
}
|
|
9043
9323
|
export {
|
|
9044
9324
|
ApiError,
|
|
9045
9325
|
ConfigError,
|
|
@@ -9056,6 +9336,7 @@ export {
|
|
|
9056
9336
|
PROVIDER_WHITELIST,
|
|
9057
9337
|
PluginManager,
|
|
9058
9338
|
RENDERERS,
|
|
9339
|
+
REVIEW_SORT_PRESETS,
|
|
9059
9340
|
SECTION_ORDER,
|
|
9060
9341
|
SENSITIVE_ARG_KEYS,
|
|
9061
9342
|
SENSITIVE_KEYS,
|
|
@@ -9202,6 +9483,7 @@ export {
|
|
|
9202
9483
|
getVitalsOverview,
|
|
9203
9484
|
getVitalsRendering,
|
|
9204
9485
|
getVitalsStartup,
|
|
9486
|
+
gradeFromPercent,
|
|
9205
9487
|
handleBreach,
|
|
9206
9488
|
importDataSafety,
|
|
9207
9489
|
importTestersFromCsv,
|
|
@@ -9256,11 +9538,18 @@ export {
|
|
|
9256
9538
|
promoteRelease,
|
|
9257
9539
|
publish,
|
|
9258
9540
|
publishEnterpriseApp,
|
|
9541
|
+
pullAchievementConfigs,
|
|
9542
|
+
pullLeaderboardConfigs,
|
|
9259
9543
|
pullListings,
|
|
9544
|
+
pushAchievementConfigs,
|
|
9545
|
+
pushLeaderboardConfigs,
|
|
9260
9546
|
pushListings,
|
|
9261
9547
|
readListingsFromDir,
|
|
9262
9548
|
readReleaseNotesForVersion,
|
|
9263
9549
|
readReleaseNotesFromDir,
|
|
9550
|
+
readinessBadgeColor,
|
|
9551
|
+
readinessBadgeMarkdown,
|
|
9552
|
+
readinessBadgeUrl,
|
|
9264
9553
|
redactAuditArgs,
|
|
9265
9554
|
redactSensitive,
|
|
9266
9555
|
refundExternalTransaction,
|
|
@@ -9288,12 +9577,16 @@ export {
|
|
|
9288
9577
|
safePathWithin,
|
|
9289
9578
|
saveStatusCache,
|
|
9290
9579
|
scaffoldPlugin,
|
|
9580
|
+
scoreReadiness,
|
|
9291
9581
|
searchAuditEvents,
|
|
9292
9582
|
searchVitalsErrors,
|
|
9293
9583
|
sendNotification,
|
|
9294
9584
|
sendWebhook,
|
|
9585
|
+
setAchievementIcon,
|
|
9586
|
+
setLeaderboardIcon,
|
|
9295
9587
|
sha256File,
|
|
9296
9588
|
sortResults,
|
|
9589
|
+
sortReviews,
|
|
9297
9590
|
startTrain,
|
|
9298
9591
|
statusHasBreach,
|
|
9299
9592
|
syncImages,
|