@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.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: mkdir8, writeFile: writeFile9 } = await import("fs/promises");
1251
- const { join: join12 } = await import("path");
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 = join12(dir, task.language, task.imageType);
1283
- await mkdir8(dirPath, { recursive: true });
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 = join12(dirPath, `${task.index}.png`);
1295
- await writeFile9(filePath, buffer);
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 mkdir5, readFile as readFile8, writeFile as writeFile5 } from "fs/promises";
4725
- import { join as join6 } from "path";
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 mkdir5(auditDir, { recursive: true, mode: 448 });
4734
- const logPath = join6(auditDir, "audit.log");
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 = join6(auditDir, "audit.log");
5032
+ const logPath = join7(auditDir, "audit.log");
4790
5033
  let content;
4791
5034
  try {
4792
- content = await readFile8(logPath, "utf-8");
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 = join6(auditDir, "audit.log");
5070
+ const logPath = join7(auditDir, "audit.log");
4828
5071
  let content;
4829
5072
  try {
4830
- content = await readFile8(logPath, "utf-8");
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 writeFile5(logPath, "", { encoding: "utf-8", mode: 384 });
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 writeFile5(logPath, keep.length > 0 ? keep.join("\n") + "\n" : "", {
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 mkdir6, writeFile as writeFile6, stat as stat5 } from "fs/promises";
4919
- import { join as join7 } from "path";
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 mkdir6(dir, { recursive: true });
4937
- await writeFile6(filePath, content, "utf-8");
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(join7(dir, ".gpcrc.json"), gpcrc + "\n", created, skipped, skipExisting);
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
- join7(dir, ".preflightrc.json"),
5211
+ join8(dir, ".preflightrc.json"),
4969
5212
  preflightrc + "\n",
4970
5213
  created,
4971
5214
  skipped,
4972
5215
  skipExisting
4973
5216
  );
4974
- const metaDir = join7(dir, "metadata", "android", "en-US");
4975
- await safeWrite(join7(metaDir, "title.txt"), "", created, skipped, skipExisting);
4976
- await safeWrite(join7(metaDir, "short_description.txt"), "", created, skipped, skipExisting);
4977
- await safeWrite(join7(metaDir, "full_description.txt"), "", created, skipped, skipExisting);
4978
- await safeWrite(join7(metaDir, "video.txt"), "", created, skipped, skipExisting);
4979
- const ssDir = join7(metaDir, "images", "phoneScreenshots");
4980
- await mkdir6(ssDir, { recursive: true });
4981
- await safeWrite(join7(ssDir, ".gitkeep"), "", created, skipped, skipExisting);
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 = join7(dir, ".github", "workflows");
4985
- await safeWrite(join7(workflowDir, "gpc-release.yml"), workflow, created, skipped, skipExisting);
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(join7(dir, ".gitlab-ci-gpc.yml"), pipeline, created, skipped, skipExisting);
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 readFile9 } from "fs/promises";
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 readFile9(path, "utf-8");
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 readFile10 } from "fs/promises";
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 readFile10(aabPath);
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 readdir4, stat as stat6, readFile as readFile11 } from "fs/promises";
6058
- import { join as join8 } from "path";
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 readdir4(dir);
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 = join8(dir, lang);
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 = join8(langDir, fileName);
6357
+ const filePath = join9(langDir, fileName);
6115
6358
  try {
6116
- const content = await readFile11(filePath, "utf-8");
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 = join8(langDir, "images", ssDir);
6398
+ const ssPath = join9(langDir, "images", ssDir);
6156
6399
  try {
6157
- const ssEntries = await readdir4(ssPath);
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 = join8(dir, defaultLang, "privacy_policy_url.txt");
6430
+ const privacyPath = join9(dir, defaultLang, "privacy_policy_url.txt");
6188
6431
  try {
6189
- const url = await readFile11(privacyPath, "utf-8");
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 readFile12 } from "fs/promises";
6457
+ import { readFile as readFile13 } from "fs/promises";
6215
6458
 
6216
6459
  // src/preflight/scan-files.ts
6217
- import { readdir as readdir5, stat as stat7 } from "fs/promises";
6218
- import { join as join9, extname as extname2 } from "path";
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 readdir5(dir);
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 = join9(dir, entry);
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 = extname2(entry).toLowerCase();
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 readFile12(filePath, "utf-8");
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 readFile13 } from "fs/promises";
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 readFile13(filePath, "utf-8");
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 readFile14 } from "fs/promises";
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 readFile14(filePath, "utf-8");
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 mkdir7, writeFile as writeFile7 } from "fs/promises";
6882
- import { join as join10 } from "path";
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 = join10(dir, "src");
6888
- const testDir = join10(dir, "tests");
6889
- await mkdir7(srcDir, { recursive: true });
6890
- await mkdir7(testDir, { recursive: true });
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 writeFile7(join10(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
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 writeFile7(join10(dir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2) + "\n");
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 writeFile7(join10(srcDir, "index.ts"), srcContent);
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 writeFile7(join10(testDir, "plugin.test.ts"), testContent);
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 extname3 } from "path";
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 = extname3(filePath).toLowerCase();
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 writeFile8 } from "fs/promises";
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 writeFile8(outputPath, bytes);
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 readFile15, stat as stat8 } from "fs/promises";
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 readFile15(filePath);
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 readFile15(configPath, "utf-8");
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 readdir6 } from "fs/promises";
8717
- import { join as join11, extname as extname4 } from "path";
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 readdir6(dir, { withFileTypes: true });
8733
- return entries.filter((e) => e.isFile() && IMAGE_EXTENSIONS.has(extname4(e.name).toLowerCase())).map((e) => e.name).sort();
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 readdir6(dir, { withFileTypes: true });
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 = join11(dir, language, imageType);
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(join11(localDir, file));
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 = join11(localDir, file);
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 `![GPC readiness](${readinessBadgeUrl(score)})`;
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,