@gpc-cli/core 0.9.74 → 0.9.76
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 +440 -126
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
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,33 @@ 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(
|
|
3138
|
+
` keywords ${analysis.keywords.slice(0, 8).map((k) => k.word).join(", ")}`
|
|
3139
|
+
);
|
|
3140
|
+
}
|
|
3141
|
+
return lines;
|
|
3142
|
+
}
|
|
3047
3143
|
function formatStatusSummary(status) {
|
|
3048
3144
|
const parts = [status.packageName];
|
|
3049
3145
|
const latestRelease = status.releases.find((r) => r.status !== "draft") ?? status.releases[0];
|
|
@@ -4607,6 +4703,8 @@ async function executeStage(apiClient, packageName, state, stageIndex) {
|
|
|
4607
4703
|
}
|
|
4608
4704
|
|
|
4609
4705
|
// src/commands/games.ts
|
|
4706
|
+
import { mkdir as mkdir5, readdir as readdir4, readFile as readFile8, writeFile as writeFile5 } from "fs/promises";
|
|
4707
|
+
import { extname as extname2, join as join6 } from "path";
|
|
4610
4708
|
async function listLeaderboards(client, packageName) {
|
|
4611
4709
|
const result = await client.leaderboards.list(packageName);
|
|
4612
4710
|
return result.items ?? [];
|
|
@@ -4707,6 +4805,151 @@ async function diffLeaderboardConfig(client, leaderboardId, localData) {
|
|
|
4707
4805
|
}
|
|
4708
4806
|
return diffs;
|
|
4709
4807
|
}
|
|
4808
|
+
var ICON_CONTENT_TYPES = {
|
|
4809
|
+
".png": "image/png",
|
|
4810
|
+
".jpg": "image/jpeg",
|
|
4811
|
+
".jpeg": "image/jpeg"
|
|
4812
|
+
};
|
|
4813
|
+
function iconContentType(filePath) {
|
|
4814
|
+
const ext = extname2(filePath).toLowerCase();
|
|
4815
|
+
const contentType = ICON_CONTENT_TYPES[ext];
|
|
4816
|
+
if (!contentType) {
|
|
4817
|
+
throw new GpcError(
|
|
4818
|
+
`Unsupported icon format: ${ext || "(no extension)"}`,
|
|
4819
|
+
"GAMES_INVALID_ICON_FORMAT",
|
|
4820
|
+
2,
|
|
4821
|
+
"Provide a .png, .jpg, or .jpeg image (Play Games icons are 512x512)."
|
|
4822
|
+
);
|
|
4823
|
+
}
|
|
4824
|
+
return contentType;
|
|
4825
|
+
}
|
|
4826
|
+
async function setAchievementIcon(client, achievementId, filePath) {
|
|
4827
|
+
validateResourceId(achievementId, "achievement");
|
|
4828
|
+
return client.images.upload(
|
|
4829
|
+
achievementId,
|
|
4830
|
+
"ACHIEVEMENT_ICON",
|
|
4831
|
+
filePath,
|
|
4832
|
+
iconContentType(filePath)
|
|
4833
|
+
);
|
|
4834
|
+
}
|
|
4835
|
+
async function setLeaderboardIcon(client, leaderboardId, filePath) {
|
|
4836
|
+
validateResourceId(leaderboardId, "leaderboard");
|
|
4837
|
+
return client.images.upload(
|
|
4838
|
+
leaderboardId,
|
|
4839
|
+
"LEADERBOARD_ICON",
|
|
4840
|
+
filePath,
|
|
4841
|
+
iconContentType(filePath)
|
|
4842
|
+
);
|
|
4843
|
+
}
|
|
4844
|
+
function safeConfigFilename(id) {
|
|
4845
|
+
return `${id.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`;
|
|
4846
|
+
}
|
|
4847
|
+
async function readConfigDir(dir) {
|
|
4848
|
+
const entries = (await readdir4(dir)).filter((f) => f.toLowerCase().endsWith(".json"));
|
|
4849
|
+
const out = [];
|
|
4850
|
+
for (const file of entries) {
|
|
4851
|
+
const raw = await readFile8(join6(dir, file), "utf-8");
|
|
4852
|
+
let parsed;
|
|
4853
|
+
try {
|
|
4854
|
+
parsed = JSON.parse(raw);
|
|
4855
|
+
} catch (err) {
|
|
4856
|
+
throw new GpcError(
|
|
4857
|
+
`Invalid JSON in ${file}: ${err instanceof Error ? err.message : String(err)}`,
|
|
4858
|
+
"GAMES_INVALID_CONFIG_JSON",
|
|
4859
|
+
2,
|
|
4860
|
+
"Each file in the sync directory must be a valid config JSON."
|
|
4861
|
+
);
|
|
4862
|
+
}
|
|
4863
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
4864
|
+
throw new GpcError(
|
|
4865
|
+
`${file} is not a config object.`,
|
|
4866
|
+
"GAMES_INVALID_CONFIG_JSON",
|
|
4867
|
+
2,
|
|
4868
|
+
"Each file must contain a single achievement or leaderboard config object."
|
|
4869
|
+
);
|
|
4870
|
+
}
|
|
4871
|
+
out.push({ file, config: parsed });
|
|
4872
|
+
}
|
|
4873
|
+
return out;
|
|
4874
|
+
}
|
|
4875
|
+
async function pushAchievementConfigs(client, applicationId, dir, options) {
|
|
4876
|
+
validateApplicationId(applicationId);
|
|
4877
|
+
const created = [];
|
|
4878
|
+
const updated = [];
|
|
4879
|
+
for (const { file, config } of await readConfigDir(dir)) {
|
|
4880
|
+
if (config.id) {
|
|
4881
|
+
if (!options?.dryRun) await client.achievements.update(config.id, config);
|
|
4882
|
+
updated.push(config.id);
|
|
4883
|
+
} else {
|
|
4884
|
+
if (options?.dryRun) {
|
|
4885
|
+
created.push(file);
|
|
4886
|
+
} else {
|
|
4887
|
+
const res = await client.achievements.insert(applicationId, config);
|
|
4888
|
+
created.push(res.id ?? file);
|
|
4889
|
+
}
|
|
4890
|
+
}
|
|
4891
|
+
}
|
|
4892
|
+
return { created, updated };
|
|
4893
|
+
}
|
|
4894
|
+
async function pullAchievementConfigs(client, applicationId, dir) {
|
|
4895
|
+
validateApplicationId(applicationId);
|
|
4896
|
+
await mkdir5(dir, { recursive: true });
|
|
4897
|
+
const written = [];
|
|
4898
|
+
let pageToken;
|
|
4899
|
+
do {
|
|
4900
|
+
const result = await client.achievements.list(applicationId, { maxResults: 200, pageToken });
|
|
4901
|
+
for (const config of result.items ?? []) {
|
|
4902
|
+
const id = config.id ?? "unknown";
|
|
4903
|
+
await writeFile5(
|
|
4904
|
+
join6(dir, safeConfigFilename(id)),
|
|
4905
|
+
JSON.stringify(config, null, 2) + "\n",
|
|
4906
|
+
"utf-8"
|
|
4907
|
+
);
|
|
4908
|
+
written.push(id);
|
|
4909
|
+
}
|
|
4910
|
+
pageToken = result.nextPageToken;
|
|
4911
|
+
} while (pageToken);
|
|
4912
|
+
return written;
|
|
4913
|
+
}
|
|
4914
|
+
async function pushLeaderboardConfigs(client, applicationId, dir, options) {
|
|
4915
|
+
validateApplicationId(applicationId);
|
|
4916
|
+
const created = [];
|
|
4917
|
+
const updated = [];
|
|
4918
|
+
for (const { file, config } of await readConfigDir(dir)) {
|
|
4919
|
+
if (config.id) {
|
|
4920
|
+
if (!options?.dryRun) await client.leaderboards.update(config.id, config);
|
|
4921
|
+
updated.push(config.id);
|
|
4922
|
+
} else {
|
|
4923
|
+
if (options?.dryRun) {
|
|
4924
|
+
created.push(file);
|
|
4925
|
+
} else {
|
|
4926
|
+
const res = await client.leaderboards.insert(applicationId, config);
|
|
4927
|
+
created.push(res.id ?? file);
|
|
4928
|
+
}
|
|
4929
|
+
}
|
|
4930
|
+
}
|
|
4931
|
+
return { created, updated };
|
|
4932
|
+
}
|
|
4933
|
+
async function pullLeaderboardConfigs(client, applicationId, dir) {
|
|
4934
|
+
validateApplicationId(applicationId);
|
|
4935
|
+
await mkdir5(dir, { recursive: true });
|
|
4936
|
+
const written = [];
|
|
4937
|
+
let pageToken;
|
|
4938
|
+
do {
|
|
4939
|
+
const result = await client.leaderboards.list(applicationId, { maxResults: 200, pageToken });
|
|
4940
|
+
for (const config of result.items ?? []) {
|
|
4941
|
+
const id = config.id ?? "unknown";
|
|
4942
|
+
await writeFile5(
|
|
4943
|
+
join6(dir, safeConfigFilename(id)),
|
|
4944
|
+
JSON.stringify(config, null, 2) + "\n",
|
|
4945
|
+
"utf-8"
|
|
4946
|
+
);
|
|
4947
|
+
written.push(id);
|
|
4948
|
+
}
|
|
4949
|
+
pageToken = result.nextPageToken;
|
|
4950
|
+
} while (pageToken);
|
|
4951
|
+
return written;
|
|
4952
|
+
}
|
|
4710
4953
|
|
|
4711
4954
|
// src/commands/enterprise.ts
|
|
4712
4955
|
async function createEnterpriseApp(client, params) {
|
|
@@ -4721,8 +4964,8 @@ async function publishEnterpriseApp(client, params) {
|
|
|
4721
4964
|
}
|
|
4722
4965
|
|
|
4723
4966
|
// src/audit.ts
|
|
4724
|
-
import { appendFile, chmod, mkdir as
|
|
4725
|
-
import { join as
|
|
4967
|
+
import { appendFile, chmod, mkdir as mkdir6, readFile as readFile9, writeFile as writeFile6 } from "fs/promises";
|
|
4968
|
+
import { join as join7 } from "path";
|
|
4726
4969
|
var auditDir = null;
|
|
4727
4970
|
function initAudit(configDir) {
|
|
4728
4971
|
auditDir = configDir;
|
|
@@ -4730,8 +4973,8 @@ function initAudit(configDir) {
|
|
|
4730
4973
|
async function writeAuditLog(entry) {
|
|
4731
4974
|
if (!auditDir) return;
|
|
4732
4975
|
try {
|
|
4733
|
-
await
|
|
4734
|
-
const logPath =
|
|
4976
|
+
await mkdir6(auditDir, { recursive: true, mode: 448 });
|
|
4977
|
+
const logPath = join7(auditDir, "audit.log");
|
|
4735
4978
|
const redactedEntry = redactAuditArgs(entry);
|
|
4736
4979
|
const line = JSON.stringify(redactedEntry) + "\n";
|
|
4737
4980
|
await appendFile(logPath, line, { encoding: "utf-8", mode: 384 });
|
|
@@ -4786,10 +5029,10 @@ function createAuditEntry(command, args, app) {
|
|
|
4786
5029
|
}
|
|
4787
5030
|
async function listAuditEvents(options) {
|
|
4788
5031
|
if (!auditDir) return [];
|
|
4789
|
-
const logPath =
|
|
5032
|
+
const logPath = join7(auditDir, "audit.log");
|
|
4790
5033
|
let content;
|
|
4791
5034
|
try {
|
|
4792
|
-
content = await
|
|
5035
|
+
content = await readFile9(logPath, "utf-8");
|
|
4793
5036
|
} catch {
|
|
4794
5037
|
return [];
|
|
4795
5038
|
}
|
|
@@ -4824,10 +5067,10 @@ async function searchAuditEvents(query) {
|
|
|
4824
5067
|
}
|
|
4825
5068
|
async function clearAuditLog(options) {
|
|
4826
5069
|
if (!auditDir) return { deleted: 0, remaining: 0 };
|
|
4827
|
-
const logPath =
|
|
5070
|
+
const logPath = join7(auditDir, "audit.log");
|
|
4828
5071
|
let content;
|
|
4829
5072
|
try {
|
|
4830
|
-
content = await
|
|
5073
|
+
content = await readFile9(logPath, "utf-8");
|
|
4831
5074
|
} catch {
|
|
4832
5075
|
return { deleted: 0, remaining: 0 };
|
|
4833
5076
|
}
|
|
@@ -4835,7 +5078,7 @@ async function clearAuditLog(options) {
|
|
|
4835
5078
|
if (!options?.before) {
|
|
4836
5079
|
const count = lines.length;
|
|
4837
5080
|
if (!options?.dryRun) {
|
|
4838
|
-
await
|
|
5081
|
+
await writeFile6(logPath, "", { encoding: "utf-8", mode: 384 });
|
|
4839
5082
|
}
|
|
4840
5083
|
return { deleted: count, remaining: 0 };
|
|
4841
5084
|
}
|
|
@@ -4855,7 +5098,7 @@ async function clearAuditLog(options) {
|
|
|
4855
5098
|
}
|
|
4856
5099
|
}
|
|
4857
5100
|
if (!options?.dryRun) {
|
|
4858
|
-
await
|
|
5101
|
+
await writeFile6(logPath, keep.length > 0 ? keep.join("\n") + "\n" : "", {
|
|
4859
5102
|
encoding: "utf-8",
|
|
4860
5103
|
mode: 384
|
|
4861
5104
|
});
|
|
@@ -4915,8 +5158,8 @@ function safePathWithin(userPath, baseDir) {
|
|
|
4915
5158
|
}
|
|
4916
5159
|
|
|
4917
5160
|
// src/commands/init.ts
|
|
4918
|
-
import { mkdir as
|
|
4919
|
-
import { join as
|
|
5161
|
+
import { mkdir as mkdir7, writeFile as writeFile7, stat as stat5 } from "fs/promises";
|
|
5162
|
+
import { join as join8 } from "path";
|
|
4920
5163
|
async function exists2(path) {
|
|
4921
5164
|
try {
|
|
4922
5165
|
await stat5(path);
|
|
@@ -4933,8 +5176,8 @@ async function safeWrite(filePath, content, created, skipped, skipExisting) {
|
|
|
4933
5176
|
}
|
|
4934
5177
|
}
|
|
4935
5178
|
const dir = filePath.substring(0, filePath.lastIndexOf("/"));
|
|
4936
|
-
await
|
|
4937
|
-
await
|
|
5179
|
+
await mkdir7(dir, { recursive: true });
|
|
5180
|
+
await writeFile7(filePath, content, "utf-8");
|
|
4938
5181
|
created.push(filePath);
|
|
4939
5182
|
}
|
|
4940
5183
|
async function initProject(options) {
|
|
@@ -4951,7 +5194,7 @@ async function initProject(options) {
|
|
|
4951
5194
|
null,
|
|
4952
5195
|
2
|
|
4953
5196
|
);
|
|
4954
|
-
await safeWrite(
|
|
5197
|
+
await safeWrite(join8(dir, ".gpcrc.json"), gpcrc + "\n", created, skipped, skipExisting);
|
|
4955
5198
|
const preflightrc = JSON.stringify(
|
|
4956
5199
|
{
|
|
4957
5200
|
failOn: "error",
|
|
@@ -4965,27 +5208,27 @@ async function initProject(options) {
|
|
|
4965
5208
|
2
|
|
4966
5209
|
);
|
|
4967
5210
|
await safeWrite(
|
|
4968
|
-
|
|
5211
|
+
join8(dir, ".preflightrc.json"),
|
|
4969
5212
|
preflightrc + "\n",
|
|
4970
5213
|
created,
|
|
4971
5214
|
skipped,
|
|
4972
5215
|
skipExisting
|
|
4973
5216
|
);
|
|
4974
|
-
const metaDir =
|
|
4975
|
-
await safeWrite(
|
|
4976
|
-
await safeWrite(
|
|
4977
|
-
await safeWrite(
|
|
4978
|
-
await safeWrite(
|
|
4979
|
-
const ssDir =
|
|
4980
|
-
await
|
|
4981
|
-
await safeWrite(
|
|
5217
|
+
const metaDir = join8(dir, "metadata", "android", "en-US");
|
|
5218
|
+
await safeWrite(join8(metaDir, "title.txt"), "", created, skipped, skipExisting);
|
|
5219
|
+
await safeWrite(join8(metaDir, "short_description.txt"), "", created, skipped, skipExisting);
|
|
5220
|
+
await safeWrite(join8(metaDir, "full_description.txt"), "", created, skipped, skipExisting);
|
|
5221
|
+
await safeWrite(join8(metaDir, "video.txt"), "", created, skipped, skipExisting);
|
|
5222
|
+
const ssDir = join8(metaDir, "images", "phoneScreenshots");
|
|
5223
|
+
await mkdir7(ssDir, { recursive: true });
|
|
5224
|
+
await safeWrite(join8(ssDir, ".gitkeep"), "", created, skipped, skipExisting);
|
|
4982
5225
|
if (ci === "github") {
|
|
4983
5226
|
const workflow = githubActionsTemplate(pkg);
|
|
4984
|
-
const workflowDir =
|
|
4985
|
-
await safeWrite(
|
|
5227
|
+
const workflowDir = join8(dir, ".github", "workflows");
|
|
5228
|
+
await safeWrite(join8(workflowDir, "gpc-release.yml"), workflow, created, skipped, skipExisting);
|
|
4986
5229
|
} else if (ci === "gitlab") {
|
|
4987
5230
|
const pipeline = gitlabCiTemplate(pkg);
|
|
4988
|
-
await safeWrite(
|
|
5231
|
+
await safeWrite(join8(dir, ".gitlab-ci-gpc.yml"), pipeline, created, skipped, skipExisting);
|
|
4989
5232
|
}
|
|
4990
5233
|
return { created, skipped };
|
|
4991
5234
|
}
|
|
@@ -5081,13 +5324,13 @@ var DEFAULT_PREFLIGHT_CONFIG = {
|
|
|
5081
5324
|
};
|
|
5082
5325
|
|
|
5083
5326
|
// src/preflight/config.ts
|
|
5084
|
-
import { readFile as
|
|
5327
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
5085
5328
|
var VALID_SEVERITIES = /* @__PURE__ */ new Set(["critical", "error", "warning", "info"]);
|
|
5086
5329
|
async function loadPreflightConfig(configPath) {
|
|
5087
5330
|
const path = configPath || ".preflightrc.json";
|
|
5088
5331
|
let raw;
|
|
5089
5332
|
try {
|
|
5090
|
-
raw = await
|
|
5333
|
+
raw = await readFile10(path, "utf-8");
|
|
5091
5334
|
} catch {
|
|
5092
5335
|
return { ...DEFAULT_PREFLIGHT_CONFIG };
|
|
5093
5336
|
}
|
|
@@ -5132,7 +5375,7 @@ async function loadPreflightConfig(configPath) {
|
|
|
5132
5375
|
}
|
|
5133
5376
|
|
|
5134
5377
|
// src/preflight/aab-reader.ts
|
|
5135
|
-
import { readFile as
|
|
5378
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
5136
5379
|
import { fromBuffer } from "yauzl";
|
|
5137
5380
|
|
|
5138
5381
|
// src/preflight/manifest-parser.ts
|
|
@@ -5330,10 +5573,10 @@ async function readAab(aabPath) {
|
|
|
5330
5573
|
const manifestPath = detectManifestPath(aabPath);
|
|
5331
5574
|
let buffer;
|
|
5332
5575
|
try {
|
|
5333
|
-
buffer = await
|
|
5576
|
+
buffer = await readFile11(aabPath);
|
|
5334
5577
|
} catch (err) {
|
|
5335
5578
|
const msg = err instanceof Error ? err.message : String(err);
|
|
5336
|
-
throw new Error(`Could not read ${aabPath}: ${msg}
|
|
5579
|
+
throw new Error(`Could not read ${aabPath}: ${msg}`, { cause: err });
|
|
5337
5580
|
}
|
|
5338
5581
|
const { zipfile, entries, manifestBuf, soHeaders } = await openAndScan(buffer, manifestPath);
|
|
5339
5582
|
zipfile.close();
|
|
@@ -5699,8 +5942,8 @@ var RESTRICTED_PERMISSIONS = [
|
|
|
5699
5942
|
permission: "android.permission.READ_CALL_LOG",
|
|
5700
5943
|
severity: "critical",
|
|
5701
5944
|
title: "READ_CALL_LOG requires declaration form",
|
|
5702
|
-
message: "READ_CALL_LOG is restricted to default phone/assistant handler apps.",
|
|
5703
|
-
suggestion: "Remove READ_CALL_LOG unless your app is a default phone or assistant handler.",
|
|
5945
|
+
message: "READ_CALL_LOG is restricted to default phone/assistant handler apps. As of July 15, 2026, account verification via phone call is no longer an accepted use case for the Call Log permissions.",
|
|
5946
|
+
suggestion: "Remove READ_CALL_LOG unless your app is a default phone or assistant handler. For phone-number verification, use the Digital Credentials API; for one-time codes, use the SMS Retriever API instead of reading the call log.",
|
|
5704
5947
|
policyUrl: "https://support.google.com/googleplay/android-developer/answer/10208820"
|
|
5705
5948
|
},
|
|
5706
5949
|
{
|
|
@@ -6054,8 +6297,8 @@ ${fileList}` + (hasPageSizeCompat ? "\nandroid:pageSizeCompat is set, so the app
|
|
|
6054
6297
|
}
|
|
6055
6298
|
|
|
6056
6299
|
// src/preflight/scanners/metadata-scanner.ts
|
|
6057
|
-
import { readdir as
|
|
6058
|
-
import { join as
|
|
6300
|
+
import { readdir as readdir5, stat as stat6, readFile as readFile12 } from "fs/promises";
|
|
6301
|
+
import { join as join9 } from "path";
|
|
6059
6302
|
var SAFE_LANG = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/;
|
|
6060
6303
|
var FILE_MAP2 = {
|
|
6061
6304
|
"title.txt": "title",
|
|
@@ -6081,7 +6324,7 @@ var metadataScanner = {
|
|
|
6081
6324
|
const findings = [];
|
|
6082
6325
|
let entries;
|
|
6083
6326
|
try {
|
|
6084
|
-
entries = await
|
|
6327
|
+
entries = await readdir5(dir);
|
|
6085
6328
|
} catch {
|
|
6086
6329
|
findings.push({
|
|
6087
6330
|
scanner: "metadata",
|
|
@@ -6106,14 +6349,14 @@ var metadataScanner = {
|
|
|
6106
6349
|
return findings;
|
|
6107
6350
|
}
|
|
6108
6351
|
for (const lang of locales) {
|
|
6109
|
-
const langDir =
|
|
6352
|
+
const langDir = join9(dir, lang);
|
|
6110
6353
|
const langStat = await stat6(langDir).catch(() => null);
|
|
6111
6354
|
if (!langStat?.isDirectory()) continue;
|
|
6112
6355
|
const fields = {};
|
|
6113
6356
|
for (const [fileName, field] of Object.entries(FILE_MAP2)) {
|
|
6114
|
-
const filePath =
|
|
6357
|
+
const filePath = join9(langDir, fileName);
|
|
6115
6358
|
try {
|
|
6116
|
-
const content = await
|
|
6359
|
+
const content = await readFile12(filePath, "utf-8");
|
|
6117
6360
|
fields[field] = content.trimEnd();
|
|
6118
6361
|
} catch {
|
|
6119
6362
|
}
|
|
@@ -6152,9 +6395,9 @@ var metadataScanner = {
|
|
|
6152
6395
|
let totalScreenshots = 0;
|
|
6153
6396
|
let phoneScreenshots = 0;
|
|
6154
6397
|
for (const ssDir of SCREENSHOT_DIRS) {
|
|
6155
|
-
const ssPath =
|
|
6398
|
+
const ssPath = join9(langDir, "images", ssDir);
|
|
6156
6399
|
try {
|
|
6157
|
-
const ssEntries = await
|
|
6400
|
+
const ssEntries = await readdir5(ssPath);
|
|
6158
6401
|
const imageFiles = ssEntries.filter((f) => /\.(png|jpe?g|webp)$/i.test(f));
|
|
6159
6402
|
totalScreenshots += imageFiles.length;
|
|
6160
6403
|
if (ssDir === "phoneScreenshots") {
|
|
@@ -6184,9 +6427,9 @@ var metadataScanner = {
|
|
|
6184
6427
|
}
|
|
6185
6428
|
}
|
|
6186
6429
|
const defaultLang = locales.includes("en-US") ? "en-US" : locales[0];
|
|
6187
|
-
const privacyPath =
|
|
6430
|
+
const privacyPath = join9(dir, defaultLang, "privacy_policy_url.txt");
|
|
6188
6431
|
try {
|
|
6189
|
-
const url = await
|
|
6432
|
+
const url = await readFile12(privacyPath, "utf-8");
|
|
6190
6433
|
if (!url.trim()) throw new Error("empty");
|
|
6191
6434
|
} catch {
|
|
6192
6435
|
findings.push({
|
|
@@ -6211,11 +6454,11 @@ var metadataScanner = {
|
|
|
6211
6454
|
};
|
|
6212
6455
|
|
|
6213
6456
|
// src/preflight/scanners/secrets-scanner.ts
|
|
6214
|
-
import { readFile as
|
|
6457
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
6215
6458
|
|
|
6216
6459
|
// src/preflight/scan-files.ts
|
|
6217
|
-
import { readdir as
|
|
6218
|
-
import { join as
|
|
6460
|
+
import { readdir as readdir6, stat as stat7 } from "fs/promises";
|
|
6461
|
+
import { join as join10, extname as extname3 } from "path";
|
|
6219
6462
|
var DEFAULT_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
6220
6463
|
".git",
|
|
6221
6464
|
"node_modules",
|
|
@@ -6232,20 +6475,20 @@ async function collectSourceFiles(dir, extensions, skipDirs = DEFAULT_SKIP_DIRS,
|
|
|
6232
6475
|
const files = [];
|
|
6233
6476
|
let entries;
|
|
6234
6477
|
try {
|
|
6235
|
-
entries = await
|
|
6478
|
+
entries = await readdir6(dir);
|
|
6236
6479
|
} catch {
|
|
6237
6480
|
return files;
|
|
6238
6481
|
}
|
|
6239
6482
|
for (const entry of entries) {
|
|
6240
6483
|
if (skipDirs.has(entry)) continue;
|
|
6241
|
-
const fullPath =
|
|
6484
|
+
const fullPath = join10(dir, entry);
|
|
6242
6485
|
const s = await stat7(fullPath).catch(() => null);
|
|
6243
6486
|
if (!s) continue;
|
|
6244
6487
|
if (s.isDirectory()) {
|
|
6245
6488
|
const sub = await collectSourceFiles(fullPath, extensions, skipDirs, maxDepth - 1);
|
|
6246
6489
|
files.push(...sub);
|
|
6247
6490
|
} else if (s.isFile()) {
|
|
6248
|
-
const ext =
|
|
6491
|
+
const ext = extname3(entry).toLowerCase();
|
|
6249
6492
|
if (extensions.has(ext) || entry.endsWith(".gradle.kts")) {
|
|
6250
6493
|
files.push(fullPath);
|
|
6251
6494
|
}
|
|
@@ -6331,7 +6574,7 @@ var secretsScanner = {
|
|
|
6331
6574
|
for (const filePath of files) {
|
|
6332
6575
|
let content;
|
|
6333
6576
|
try {
|
|
6334
|
-
content = await
|
|
6577
|
+
content = await readFile13(filePath, "utf-8");
|
|
6335
6578
|
} catch {
|
|
6336
6579
|
continue;
|
|
6337
6580
|
}
|
|
@@ -6359,7 +6602,7 @@ var secretsScanner = {
|
|
|
6359
6602
|
};
|
|
6360
6603
|
|
|
6361
6604
|
// src/preflight/scanners/billing-scanner.ts
|
|
6362
|
-
import { readFile as
|
|
6605
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
6363
6606
|
var BILLING_PATTERNS = [
|
|
6364
6607
|
{
|
|
6365
6608
|
ruleId: "billing-stripe-sdk",
|
|
@@ -6420,7 +6663,7 @@ var billingScanner = {
|
|
|
6420
6663
|
for (const filePath of files) {
|
|
6421
6664
|
let content;
|
|
6422
6665
|
try {
|
|
6423
|
-
content = await
|
|
6666
|
+
content = await readFile14(filePath, "utf-8");
|
|
6424
6667
|
} catch {
|
|
6425
6668
|
continue;
|
|
6426
6669
|
}
|
|
@@ -6446,7 +6689,7 @@ var billingScanner = {
|
|
|
6446
6689
|
};
|
|
6447
6690
|
|
|
6448
6691
|
// src/preflight/scanners/privacy-scanner.ts
|
|
6449
|
-
import { readFile as
|
|
6692
|
+
import { readFile as readFile15 } from "fs/promises";
|
|
6450
6693
|
var TRACKING_SDKS = [
|
|
6451
6694
|
{
|
|
6452
6695
|
name: "Facebook SDK",
|
|
@@ -6486,7 +6729,7 @@ var privacyScanner = {
|
|
|
6486
6729
|
for (const filePath of files) {
|
|
6487
6730
|
let content;
|
|
6488
6731
|
try {
|
|
6489
|
-
content = await
|
|
6732
|
+
content = await readFile15(filePath, "utf-8");
|
|
6490
6733
|
} catch {
|
|
6491
6734
|
continue;
|
|
6492
6735
|
}
|
|
@@ -6627,6 +6870,15 @@ var policyScanner = {
|
|
|
6627
6870
|
policyUrl: "https://support.google.com/googleplay/android-developer/answer/9876937"
|
|
6628
6871
|
});
|
|
6629
6872
|
}
|
|
6873
|
+
findings.push({
|
|
6874
|
+
scanner: "policy",
|
|
6875
|
+
ruleId: "policy-developer-verification",
|
|
6876
|
+
severity: "info",
|
|
6877
|
+
title: "Developer verification enforcement (Sep 30, 2026)",
|
|
6878
|
+
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. Separately, as of July 15, 2026, all apps on Google Play must be registered in Play Console to meet these requirements, and unregistered apps risk removal from Google Play. Apps using Play App Signing are auto-registered.",
|
|
6879
|
+
suggestion: "Run `gpc verify checklist` to review readiness, including registration of every app on your account. This is a distribution-market and account-registration reminder, not an AAB/APK defect.",
|
|
6880
|
+
policyUrl: "https://developer.android.com/developer-verification"
|
|
6881
|
+
});
|
|
6630
6882
|
if (perms.has("android.permission.SYSTEM_ALERT_WINDOW")) {
|
|
6631
6883
|
findings.push({
|
|
6632
6884
|
scanner: "policy",
|
|
@@ -6837,57 +7089,17 @@ async function runPreflight(options) {
|
|
|
6837
7089
|
};
|
|
6838
7090
|
}
|
|
6839
7091
|
|
|
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
7092
|
// src/commands/plugin-scaffold.ts
|
|
6881
|
-
import { mkdir as
|
|
6882
|
-
import { join as
|
|
7093
|
+
import { mkdir as mkdir8, writeFile as writeFile8 } from "fs/promises";
|
|
7094
|
+
import { join as join11 } from "path";
|
|
6883
7095
|
async function scaffoldPlugin(options) {
|
|
6884
7096
|
const { name, dir, description = `GPC plugin: ${name}` } = options;
|
|
6885
7097
|
const pluginName = name.startsWith("gpc-plugin-") ? name : `gpc-plugin-${name}`;
|
|
6886
7098
|
const shortName = pluginName.replace(/^gpc-plugin-/, "");
|
|
6887
|
-
const srcDir =
|
|
6888
|
-
const testDir =
|
|
6889
|
-
await
|
|
6890
|
-
await
|
|
7099
|
+
const srcDir = join11(dir, "src");
|
|
7100
|
+
const testDir = join11(dir, "tests");
|
|
7101
|
+
await mkdir8(srcDir, { recursive: true });
|
|
7102
|
+
await mkdir8(testDir, { recursive: true });
|
|
6891
7103
|
const files = [];
|
|
6892
7104
|
const pkg = {
|
|
6893
7105
|
name: pluginName,
|
|
@@ -6921,7 +7133,7 @@ async function scaffoldPlugin(options) {
|
|
|
6921
7133
|
vitest: "^3.0.0"
|
|
6922
7134
|
}
|
|
6923
7135
|
};
|
|
6924
|
-
await
|
|
7136
|
+
await writeFile8(join11(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
|
|
6925
7137
|
files.push("package.json");
|
|
6926
7138
|
const tsconfig = {
|
|
6927
7139
|
compilerOptions: {
|
|
@@ -6937,7 +7149,7 @@ async function scaffoldPlugin(options) {
|
|
|
6937
7149
|
},
|
|
6938
7150
|
include: ["src"]
|
|
6939
7151
|
};
|
|
6940
|
-
await
|
|
7152
|
+
await writeFile8(join11(dir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2) + "\n");
|
|
6941
7153
|
files.push("tsconfig.json");
|
|
6942
7154
|
const srcContent = `import { definePlugin } from "@gpc-cli/plugin-sdk";
|
|
6943
7155
|
import type { CommandEvent, CommandResult } from "@gpc-cli/plugin-sdk";
|
|
@@ -6970,7 +7182,7 @@ export const plugin = definePlugin({
|
|
|
6970
7182
|
},
|
|
6971
7183
|
});
|
|
6972
7184
|
`;
|
|
6973
|
-
await
|
|
7185
|
+
await writeFile8(join11(srcDir, "index.ts"), srcContent);
|
|
6974
7186
|
files.push("src/index.ts");
|
|
6975
7187
|
const testContent = `import { describe, it, expect, vi } from "vitest";
|
|
6976
7188
|
import { plugin } from "../src/index";
|
|
@@ -6995,7 +7207,7 @@ describe("${pluginName}", () => {
|
|
|
6995
7207
|
});
|
|
6996
7208
|
});
|
|
6997
7209
|
`;
|
|
6998
|
-
await
|
|
7210
|
+
await writeFile8(join11(testDir, "plugin.test.ts"), testContent);
|
|
6999
7211
|
files.push("tests/plugin.test.ts");
|
|
7000
7212
|
return { dir, files };
|
|
7001
7213
|
}
|
|
@@ -7111,7 +7323,7 @@ async function sendWebhook(config, payload, target) {
|
|
|
7111
7323
|
}
|
|
7112
7324
|
|
|
7113
7325
|
// src/commands/internal-sharing.ts
|
|
7114
|
-
import { extname as
|
|
7326
|
+
import { extname as extname4 } from "path";
|
|
7115
7327
|
async function uploadInternalSharing(client, packageName, filePath, fileType) {
|
|
7116
7328
|
const resolvedType = fileType ?? detectFileType(filePath);
|
|
7117
7329
|
const validation = await validateUploadFile(filePath);
|
|
@@ -7138,7 +7350,7 @@ ${validation.errors.join("\n")}`,
|
|
|
7138
7350
|
};
|
|
7139
7351
|
}
|
|
7140
7352
|
function detectFileType(filePath) {
|
|
7141
|
-
const ext =
|
|
7353
|
+
const ext = extname4(filePath).toLowerCase();
|
|
7142
7354
|
if (ext === ".aab") return "bundle";
|
|
7143
7355
|
if (ext === ".apk") return "apk";
|
|
7144
7356
|
throw new GpcError(
|
|
@@ -7150,7 +7362,7 @@ function detectFileType(filePath) {
|
|
|
7150
7362
|
}
|
|
7151
7363
|
|
|
7152
7364
|
// src/commands/generated-apks.ts
|
|
7153
|
-
import { writeFile as
|
|
7365
|
+
import { writeFile as writeFile9 } from "fs/promises";
|
|
7154
7366
|
async function listGeneratedApks(client, packageName, versionCode) {
|
|
7155
7367
|
if (!Number.isInteger(versionCode) || versionCode <= 0) {
|
|
7156
7368
|
throw new GpcError(
|
|
@@ -7181,12 +7393,12 @@ async function downloadGeneratedApk(client, packageName, versionCode, apkId, out
|
|
|
7181
7393
|
}
|
|
7182
7394
|
const buffer = await client.generatedApks.download(packageName, versionCode, apkId);
|
|
7183
7395
|
const bytes = new Uint8Array(buffer);
|
|
7184
|
-
await
|
|
7396
|
+
await writeFile9(outputPath, bytes);
|
|
7185
7397
|
return { path: outputPath, sizeBytes: bytes.byteLength };
|
|
7186
7398
|
}
|
|
7187
7399
|
|
|
7188
7400
|
// src/commands/bundle-analysis.ts
|
|
7189
|
-
import { readFile as
|
|
7401
|
+
import { readFile as readFile16, stat as stat8 } from "fs/promises";
|
|
7190
7402
|
var EOCD_SIGNATURE = 101010256;
|
|
7191
7403
|
var CD_SIGNATURE = 33639248;
|
|
7192
7404
|
var MODULE_SUBDIRS = /* @__PURE__ */ new Set(["dex", "manifest", "res", "assets", "lib", "resources.pb", "root"]);
|
|
@@ -7266,7 +7478,7 @@ async function analyzeBundle(filePath) {
|
|
|
7266
7478
|
if (!fileInfo || !fileInfo.isFile()) {
|
|
7267
7479
|
throw new Error(`File not found: ${filePath}`);
|
|
7268
7480
|
}
|
|
7269
|
-
const buf = await
|
|
7481
|
+
const buf = await readFile16(filePath);
|
|
7270
7482
|
const cdEntries = parseCentralDirectory(buf);
|
|
7271
7483
|
const fileType = detectFileType2(filePath);
|
|
7272
7484
|
const isAab = fileType === "aab";
|
|
@@ -7352,7 +7564,7 @@ function topFiles(analysis, n = 20) {
|
|
|
7352
7564
|
async function checkBundleSize(analysis, configPath = ".bundlesize.json") {
|
|
7353
7565
|
let config;
|
|
7354
7566
|
try {
|
|
7355
|
-
const raw = await
|
|
7567
|
+
const raw = await readFile16(configPath, "utf-8");
|
|
7356
7568
|
config = JSON.parse(raw);
|
|
7357
7569
|
} catch {
|
|
7358
7570
|
return { passed: true, violations: [] };
|
|
@@ -8713,8 +8925,8 @@ async function sha256File(filePath) {
|
|
|
8713
8925
|
}
|
|
8714
8926
|
|
|
8715
8927
|
// src/commands/image-sync.ts
|
|
8716
|
-
import { readdir as
|
|
8717
|
-
import { join as
|
|
8928
|
+
import { readdir as readdir7 } from "fs/promises";
|
|
8929
|
+
import { join as join12, extname as extname5 } from "path";
|
|
8718
8930
|
import { PlayApiError } from "@gpc-cli/api";
|
|
8719
8931
|
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg"]);
|
|
8720
8932
|
var ALL_IMAGE_TYPES2 = [
|
|
@@ -8729,15 +8941,15 @@ var ALL_IMAGE_TYPES2 = [
|
|
|
8729
8941
|
];
|
|
8730
8942
|
async function scanLocalImages(dir) {
|
|
8731
8943
|
try {
|
|
8732
|
-
const entries = await
|
|
8733
|
-
return entries.filter((e) => e.isFile() && IMAGE_EXTENSIONS.has(
|
|
8944
|
+
const entries = await readdir7(dir, { withFileTypes: true });
|
|
8945
|
+
return entries.filter((e) => e.isFile() && IMAGE_EXTENSIONS.has(extname5(e.name).toLowerCase())).map((e) => e.name).sort();
|
|
8734
8946
|
} catch {
|
|
8735
8947
|
return [];
|
|
8736
8948
|
}
|
|
8737
8949
|
}
|
|
8738
8950
|
async function scanLanguages(dir) {
|
|
8739
8951
|
try {
|
|
8740
|
-
const entries = await
|
|
8952
|
+
const entries = await readdir7(dir, { withFileTypes: true });
|
|
8741
8953
|
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
8742
8954
|
} catch {
|
|
8743
8955
|
return [];
|
|
@@ -8762,7 +8974,7 @@ async function syncImages(client, packageName, dir, options) {
|
|
|
8762
8974
|
try {
|
|
8763
8975
|
for (const language of languages) {
|
|
8764
8976
|
for (const imageType of imageTypes) {
|
|
8765
|
-
const localDir =
|
|
8977
|
+
const localDir = join12(dir, language, imageType);
|
|
8766
8978
|
const localFiles = await scanLocalImages(localDir);
|
|
8767
8979
|
let remoteImages;
|
|
8768
8980
|
try {
|
|
@@ -8777,7 +8989,7 @@ async function syncImages(client, packageName, dir, options) {
|
|
|
8777
8989
|
const remoteSha256Set = new Set(remoteImages.map((img) => img.sha256.toLowerCase()));
|
|
8778
8990
|
const localHashes = /* @__PURE__ */ new Map();
|
|
8779
8991
|
for (const file of localFiles) {
|
|
8780
|
-
const hash = await sha256File(
|
|
8992
|
+
const hash = await sha256File(join12(localDir, file));
|
|
8781
8993
|
localHashes.set(file, hash);
|
|
8782
8994
|
}
|
|
8783
8995
|
const localSha256Set = new Set(localHashes.values());
|
|
@@ -8805,7 +9017,7 @@ async function syncImages(client, packageName, dir, options) {
|
|
|
8805
9017
|
details.push({ language, imageType, file, action: "skip", reason: "sha256 match" });
|
|
8806
9018
|
} else {
|
|
8807
9019
|
if (!options?.dryRun) {
|
|
8808
|
-
const filePath =
|
|
9020
|
+
const filePath = join12(localDir, file);
|
|
8809
9021
|
const check = await validateImage(filePath, imageType);
|
|
8810
9022
|
if (!check.valid) {
|
|
8811
9023
|
throw new GpcError(
|
|
@@ -8964,6 +9176,15 @@ function buildChecklist(input) {
|
|
|
8964
9176
|
answers
|
|
8965
9177
|
)
|
|
8966
9178
|
);
|
|
9179
|
+
items.push(
|
|
9180
|
+
resolveManualStep(
|
|
9181
|
+
"enforcement-markets",
|
|
9182
|
+
"Verification enforcement timeline reviewed",
|
|
9183
|
+
"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.",
|
|
9184
|
+
VERIFICATION_PAGE,
|
|
9185
|
+
answers
|
|
9186
|
+
)
|
|
9187
|
+
);
|
|
8967
9188
|
items.push(
|
|
8968
9189
|
resolveManualStep(
|
|
8969
9190
|
"auto-registration-reviewed",
|
|
@@ -8973,6 +9194,15 @@ function buildChecklist(input) {
|
|
|
8973
9194
|
answers
|
|
8974
9195
|
)
|
|
8975
9196
|
);
|
|
9197
|
+
items.push(
|
|
9198
|
+
resolveManualStep(
|
|
9199
|
+
"all-apps-registered",
|
|
9200
|
+
"All Play apps registered in Play Console",
|
|
9201
|
+
"As of July 15, 2026, every app on your account must be registered in Play Console to meet Android developer verification requirements. Unregistered apps risk removal from Google Play. Apps using Play App Signing are auto-registered; register any others manually.",
|
|
9202
|
+
PLAY_CONSOLE_SETTINGS,
|
|
9203
|
+
answers
|
|
9204
|
+
)
|
|
9205
|
+
);
|
|
8976
9206
|
if (input.appAccessible !== void 0) {
|
|
8977
9207
|
items.push({
|
|
8978
9208
|
id: "app-accessible",
|
|
@@ -9040,6 +9270,77 @@ function renderChecklistMarkdown(result, accountEmail) {
|
|
|
9040
9270
|
lines.push("");
|
|
9041
9271
|
return lines.join("\n");
|
|
9042
9272
|
}
|
|
9273
|
+
|
|
9274
|
+
// src/commands/readiness.ts
|
|
9275
|
+
var STATUS_FRACTION = {
|
|
9276
|
+
pass: 1,
|
|
9277
|
+
warn: 0.5,
|
|
9278
|
+
fail: 0
|
|
9279
|
+
};
|
|
9280
|
+
function gradeFromPercent(percent) {
|
|
9281
|
+
if (percent >= 90) return "A";
|
|
9282
|
+
if (percent >= 80) return "B";
|
|
9283
|
+
if (percent >= 70) return "C";
|
|
9284
|
+
if (percent >= 60) return "D";
|
|
9285
|
+
return "F";
|
|
9286
|
+
}
|
|
9287
|
+
function scoreReadiness(signals) {
|
|
9288
|
+
let earned = 0;
|
|
9289
|
+
let possible = 0;
|
|
9290
|
+
const breakdown = [];
|
|
9291
|
+
const suggestions = [];
|
|
9292
|
+
for (const signal of signals) {
|
|
9293
|
+
if (signal.status === "na") {
|
|
9294
|
+
breakdown.push({
|
|
9295
|
+
key: signal.key,
|
|
9296
|
+
label: signal.label,
|
|
9297
|
+
weight: signal.weight,
|
|
9298
|
+
status: "na",
|
|
9299
|
+
points: 0,
|
|
9300
|
+
detail: signal.detail
|
|
9301
|
+
});
|
|
9302
|
+
continue;
|
|
9303
|
+
}
|
|
9304
|
+
const points = signal.weight * STATUS_FRACTION[signal.status];
|
|
9305
|
+
earned += points;
|
|
9306
|
+
possible += signal.weight;
|
|
9307
|
+
breakdown.push({
|
|
9308
|
+
key: signal.key,
|
|
9309
|
+
label: signal.label,
|
|
9310
|
+
weight: signal.weight,
|
|
9311
|
+
status: signal.status,
|
|
9312
|
+
points,
|
|
9313
|
+
detail: signal.detail
|
|
9314
|
+
});
|
|
9315
|
+
if (signal.status !== "pass" && signal.suggestion) {
|
|
9316
|
+
suggestions.push(signal.suggestion);
|
|
9317
|
+
}
|
|
9318
|
+
}
|
|
9319
|
+
const evaluated = possible > 0;
|
|
9320
|
+
const percent = evaluated ? Math.round(earned / possible * 100) : 0;
|
|
9321
|
+
const grade = evaluated ? gradeFromPercent(percent) : "F";
|
|
9322
|
+
return { grade, percent, evaluated, earned, possible, breakdown, suggestions };
|
|
9323
|
+
}
|
|
9324
|
+
var GRADE_COLOR = {
|
|
9325
|
+
A: "brightgreen",
|
|
9326
|
+
B: "green",
|
|
9327
|
+
C: "yellow",
|
|
9328
|
+
D: "orange",
|
|
9329
|
+
F: "red"
|
|
9330
|
+
};
|
|
9331
|
+
function readinessBadgeColor(grade) {
|
|
9332
|
+
return GRADE_COLOR[grade];
|
|
9333
|
+
}
|
|
9334
|
+
function readinessBadgeUrl(score) {
|
|
9335
|
+
const label = "GPC readiness";
|
|
9336
|
+
const message = score.evaluated ? `${score.grade} (${score.percent}%)` : "not evaluated";
|
|
9337
|
+
const color = score.evaluated ? readinessBadgeColor(score.grade) : "lightgrey";
|
|
9338
|
+
const enc = (segment) => encodeURIComponent(segment.replace(/-/g, "--").replace(/_/g, "__"));
|
|
9339
|
+
return `https://img.shields.io/badge/${enc(label)}-${enc(message)}-${color}`;
|
|
9340
|
+
}
|
|
9341
|
+
function readinessBadgeMarkdown(score) {
|
|
9342
|
+
return `})`;
|
|
9343
|
+
}
|
|
9043
9344
|
export {
|
|
9044
9345
|
ApiError,
|
|
9045
9346
|
ConfigError,
|
|
@@ -9056,6 +9357,7 @@ export {
|
|
|
9056
9357
|
PROVIDER_WHITELIST,
|
|
9057
9358
|
PluginManager,
|
|
9058
9359
|
RENDERERS,
|
|
9360
|
+
REVIEW_SORT_PRESETS,
|
|
9059
9361
|
SECTION_ORDER,
|
|
9060
9362
|
SENSITIVE_ARG_KEYS,
|
|
9061
9363
|
SENSITIVE_KEYS,
|
|
@@ -9202,6 +9504,7 @@ export {
|
|
|
9202
9504
|
getVitalsOverview,
|
|
9203
9505
|
getVitalsRendering,
|
|
9204
9506
|
getVitalsStartup,
|
|
9507
|
+
gradeFromPercent,
|
|
9205
9508
|
handleBreach,
|
|
9206
9509
|
importDataSafety,
|
|
9207
9510
|
importTestersFromCsv,
|
|
@@ -9256,11 +9559,18 @@ export {
|
|
|
9256
9559
|
promoteRelease,
|
|
9257
9560
|
publish,
|
|
9258
9561
|
publishEnterpriseApp,
|
|
9562
|
+
pullAchievementConfigs,
|
|
9563
|
+
pullLeaderboardConfigs,
|
|
9259
9564
|
pullListings,
|
|
9565
|
+
pushAchievementConfigs,
|
|
9566
|
+
pushLeaderboardConfigs,
|
|
9260
9567
|
pushListings,
|
|
9261
9568
|
readListingsFromDir,
|
|
9262
9569
|
readReleaseNotesForVersion,
|
|
9263
9570
|
readReleaseNotesFromDir,
|
|
9571
|
+
readinessBadgeColor,
|
|
9572
|
+
readinessBadgeMarkdown,
|
|
9573
|
+
readinessBadgeUrl,
|
|
9264
9574
|
redactAuditArgs,
|
|
9265
9575
|
redactSensitive,
|
|
9266
9576
|
refundExternalTransaction,
|
|
@@ -9288,12 +9598,16 @@ export {
|
|
|
9288
9598
|
safePathWithin,
|
|
9289
9599
|
saveStatusCache,
|
|
9290
9600
|
scaffoldPlugin,
|
|
9601
|
+
scoreReadiness,
|
|
9291
9602
|
searchAuditEvents,
|
|
9292
9603
|
searchVitalsErrors,
|
|
9293
9604
|
sendNotification,
|
|
9294
9605
|
sendWebhook,
|
|
9606
|
+
setAchievementIcon,
|
|
9607
|
+
setLeaderboardIcon,
|
|
9295
9608
|
sha256File,
|
|
9296
9609
|
sortResults,
|
|
9610
|
+
sortReviews,
|
|
9297
9611
|
startTrain,
|
|
9298
9612
|
statusHasBreach,
|
|
9299
9613
|
syncImages,
|