@gpc-cli/core 0.9.73 → 0.9.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,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 mkdir5, readFile as readFile8, writeFile as writeFile5 } from "fs/promises";
4725
- import { join as join6 } from "path";
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 mkdir5(auditDir, { recursive: true, mode: 448 });
4734
- const logPath = join6(auditDir, "audit.log");
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 = join6(auditDir, "audit.log");
5020
+ const logPath = join7(auditDir, "audit.log");
4790
5021
  let content;
4791
5022
  try {
4792
- content = await readFile8(logPath, "utf-8");
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 = join6(auditDir, "audit.log");
5058
+ const logPath = join7(auditDir, "audit.log");
4828
5059
  let content;
4829
5060
  try {
4830
- content = await readFile8(logPath, "utf-8");
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 writeFile5(logPath, "", { encoding: "utf-8", mode: 384 });
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 writeFile5(logPath, keep.length > 0 ? keep.join("\n") + "\n" : "", {
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 mkdir6, writeFile as writeFile6, stat as stat5 } from "fs/promises";
4919
- import { join as join7 } from "path";
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 mkdir6(dir, { recursive: true });
4937
- await writeFile6(filePath, content, "utf-8");
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(join7(dir, ".gpcrc.json"), gpcrc + "\n", created, skipped, skipExisting);
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
- join7(dir, ".preflightrc.json"),
5199
+ join8(dir, ".preflightrc.json"),
4969
5200
  preflightrc + "\n",
4970
5201
  created,
4971
5202
  skipped,
4972
5203
  skipExisting
4973
5204
  );
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);
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 = join7(dir, ".github", "workflows");
4985
- await safeWrite(join7(workflowDir, "gpc-release.yml"), workflow, created, skipped, skipExisting);
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(join7(dir, ".gitlab-ci-gpc.yml"), pipeline, created, skipped, skipExisting);
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 readFile9 } from "fs/promises";
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 readFile9(path, "utf-8");
5321
+ raw = await readFile10(path, "utf-8");
5091
5322
  } catch {
5092
5323
  return { ...DEFAULT_PREFLIGHT_CONFIG };
5093
5324
  }
@@ -5132,7 +5363,8 @@ async function loadPreflightConfig(configPath) {
5132
5363
  }
5133
5364
 
5134
5365
  // src/preflight/aab-reader.ts
5135
- import { open as yauzlOpen } from "yauzl";
5366
+ import { readFile as readFile11 } from "fs/promises";
5367
+ import { fromBuffer } from "yauzl";
5136
5368
 
5137
5369
  // src/preflight/manifest-parser.ts
5138
5370
  import protobuf from "protobufjs";
@@ -5321,12 +5553,20 @@ var AAB_MANIFEST_PATH = "base/manifest/AndroidManifest.xml";
5321
5553
  var APK_MANIFEST_PATH = "AndroidManifest.xml";
5322
5554
  var SO_HEADER_BYTES = 4096;
5323
5555
  var SO_PATH_RE = /(?:^|\/)lib\/[^/]+\/[^/]+\.so$/;
5556
+ var SCAN_TIMEOUT_MS = 6e4;
5324
5557
  function detectManifestPath(filePath) {
5325
5558
  return filePath.toLowerCase().endsWith(".apk") ? APK_MANIFEST_PATH : AAB_MANIFEST_PATH;
5326
5559
  }
5327
5560
  async function readAab(aabPath) {
5328
5561
  const manifestPath = detectManifestPath(aabPath);
5329
- const { zipfile, entries, manifestBuf, soHeaders } = await openAndScan(aabPath, manifestPath);
5562
+ let buffer;
5563
+ try {
5564
+ buffer = await readFile11(aabPath);
5565
+ } catch (err) {
5566
+ const msg = err instanceof Error ? err.message : String(err);
5567
+ throw new Error(`Could not read ${aabPath}: ${msg}`, { cause: err });
5568
+ }
5569
+ const { zipfile, entries, manifestBuf, soHeaders } = await openAndScan(buffer, manifestPath);
5330
5570
  zipfile.close();
5331
5571
  if (!manifestBuf) {
5332
5572
  const fileType = aabPath.toLowerCase().endsWith(".apk") ? "APK" : "AAB";
@@ -5363,98 +5603,125 @@ function createFallbackManifest() {
5363
5603
  providers: []
5364
5604
  };
5365
5605
  }
5366
- function openAndScan(aabPath, manifestPath = AAB_MANIFEST_PATH) {
5606
+ function openAndScan(buffer, manifestPath = AAB_MANIFEST_PATH) {
5367
5607
  return new Promise((resolve2, reject) => {
5368
- yauzlOpen(aabPath, { lazyEntries: true, autoClose: false }, (err, zipfile) => {
5369
- if (err || !zipfile) {
5370
- reject(err ?? new Error("Failed to open AAB file"));
5371
- return;
5608
+ let settled = false;
5609
+ const timer = setTimeout(() => {
5610
+ if (!settled) {
5611
+ settled = true;
5612
+ reject(new Error("Timed out reading the archive. The file may be corrupt or truncated."));
5372
5613
  }
5373
- const entries = [];
5374
- const soHeaders = /* @__PURE__ */ new Map();
5375
- let manifestBuf = null;
5376
- let rejected = false;
5377
- let pendingStreams = 0;
5378
- let entrysDone = false;
5379
- function fail(error) {
5380
- if (!rejected) {
5381
- rejected = true;
5382
- zipfile.close();
5383
- reject(error);
5614
+ }, SCAN_TIMEOUT_MS);
5615
+ function settleResolve(value) {
5616
+ if (settled) return;
5617
+ settled = true;
5618
+ clearTimeout(timer);
5619
+ resolve2(value);
5620
+ }
5621
+ function settleReject(error) {
5622
+ if (settled) return;
5623
+ settled = true;
5624
+ clearTimeout(timer);
5625
+ reject(error);
5626
+ }
5627
+ try {
5628
+ fromBuffer(buffer, { lazyEntries: true }, (err, zipfile) => {
5629
+ if (err || !zipfile) {
5630
+ settleReject(err ?? new Error("Failed to open AAB file"));
5631
+ return;
5384
5632
  }
5385
- }
5386
- function maybeResolve() {
5387
- if (!rejected && entrysDone && pendingStreams === 0) {
5388
- resolve2({ zipfile, entries, manifestBuf, soHeaders });
5633
+ const entries = [];
5634
+ const soHeaders = /* @__PURE__ */ new Map();
5635
+ let manifestBuf = null;
5636
+ let pendingStreams = 0;
5637
+ let entrysDone = false;
5638
+ function fail(error) {
5639
+ if (settled) return;
5640
+ try {
5641
+ zipfile.close();
5642
+ } catch {
5643
+ }
5644
+ settleReject(error);
5389
5645
  }
5390
- }
5391
- function readEntryStream(entry, onData, maxBytes) {
5392
- pendingStreams++;
5393
- zipfile.openReadStream(entry, (streamErr, stream) => {
5394
- if (streamErr || !stream) {
5395
- fail(streamErr ?? new Error(`Failed to read ${entry.fileName}`));
5396
- return;
5646
+ function maybeResolve() {
5647
+ if (!settled && entrysDone && pendingStreams === 0) {
5648
+ settleResolve({ zipfile, entries, manifestBuf, soHeaders });
5397
5649
  }
5398
- const chunks = [];
5399
- let totalBytes = 0;
5400
- let done = false;
5401
- stream.on("data", (chunk) => {
5402
- chunks.push(chunk);
5403
- totalBytes += chunk.length;
5404
- if (maxBytes && totalBytes >= maxBytes) {
5405
- stream.destroy();
5406
- }
5407
- });
5408
- stream.on("error", (e) => {
5409
- if (maxBytes && totalBytes >= maxBytes) return;
5410
- fail(e);
5411
- });
5412
- const finish = () => {
5413
- if (done) return;
5414
- done = true;
5415
- const buf = Buffer.concat(chunks);
5416
- onData(maxBytes ? buf.subarray(0, maxBytes) : buf);
5417
- pendingStreams--;
5418
- maybeResolve();
5419
- };
5420
- stream.on("end", finish);
5421
- stream.on("close", () => {
5422
- if (totalBytes > 0 && maxBytes) finish();
5423
- });
5424
- });
5425
- }
5426
- zipfile.on("error", fail);
5427
- zipfile.on("entry", (entry) => {
5428
- if (rejected) return;
5429
- const path = entry.fileName;
5430
- if (!path.endsWith("/")) {
5431
- entries.push({
5432
- path,
5433
- compressedSize: entry.compressedSize,
5434
- uncompressedSize: entry.uncompressedSize
5435
- });
5436
5650
  }
5437
- if (path === manifestPath) {
5438
- readEntryStream(entry, (buf) => {
5439
- manifestBuf = buf;
5651
+ function readEntryStream(entry, onData, maxBytes) {
5652
+ pendingStreams++;
5653
+ zipfile.openReadStream(entry, (streamErr, stream) => {
5654
+ if (streamErr || !stream) {
5655
+ fail(streamErr ?? new Error(`Failed to read ${entry.fileName}`));
5656
+ return;
5657
+ }
5658
+ const chunks = [];
5659
+ let totalBytes = 0;
5660
+ let done = false;
5661
+ stream.on("data", (chunk) => {
5662
+ chunks.push(chunk);
5663
+ totalBytes += chunk.length;
5664
+ if (maxBytes && totalBytes >= maxBytes) {
5665
+ stream.destroy();
5666
+ }
5667
+ });
5668
+ stream.on("error", (e) => {
5669
+ if (maxBytes && totalBytes >= maxBytes) return;
5670
+ fail(e);
5671
+ });
5672
+ const finish = () => {
5673
+ if (done) return;
5674
+ done = true;
5675
+ const buf = Buffer.concat(chunks);
5676
+ onData(maxBytes ? buf.subarray(0, maxBytes) : buf);
5677
+ pendingStreams--;
5678
+ maybeResolve();
5679
+ };
5680
+ stream.on("end", finish);
5681
+ stream.on("close", () => {
5682
+ if (totalBytes > 0 && maxBytes) finish();
5683
+ });
5440
5684
  });
5441
- } else if (SO_PATH_RE.test(path)) {
5442
- readEntryStream(
5443
- entry,
5444
- (buf) => {
5445
- soHeaders.set(path, buf);
5446
- },
5447
- SO_HEADER_BYTES
5448
- );
5449
5685
  }
5686
+ zipfile.on("error", fail);
5687
+ zipfile.on("entry", (entry) => {
5688
+ if (settled) return;
5689
+ try {
5690
+ const path = entry.fileName;
5691
+ if (!path.endsWith("/")) {
5692
+ entries.push({
5693
+ path,
5694
+ compressedSize: entry.compressedSize,
5695
+ uncompressedSize: entry.uncompressedSize
5696
+ });
5697
+ }
5698
+ if (path === manifestPath) {
5699
+ readEntryStream(entry, (buf) => {
5700
+ manifestBuf = buf;
5701
+ });
5702
+ } else if (SO_PATH_RE.test(path)) {
5703
+ readEntryStream(
5704
+ entry,
5705
+ (buf) => {
5706
+ soHeaders.set(path, buf);
5707
+ },
5708
+ SO_HEADER_BYTES
5709
+ );
5710
+ }
5711
+ zipfile.readEntry();
5712
+ } catch (e) {
5713
+ fail(e instanceof Error ? e : new Error(String(e)));
5714
+ }
5715
+ });
5716
+ zipfile.on("end", () => {
5717
+ entrysDone = true;
5718
+ maybeResolve();
5719
+ });
5450
5720
  zipfile.readEntry();
5451
5721
  });
5452
- zipfile.on("end", () => {
5453
- entrysDone = true;
5454
- maybeResolve();
5455
- });
5456
- zipfile.readEntry();
5457
- });
5722
+ } catch (err) {
5723
+ settleReject(err instanceof Error ? err : new Error(String(err)));
5724
+ }
5458
5725
  });
5459
5726
  }
5460
5727
 
@@ -6018,8 +6285,8 @@ ${fileList}` + (hasPageSizeCompat ? "\nandroid:pageSizeCompat is set, so the app
6018
6285
  }
6019
6286
 
6020
6287
  // src/preflight/scanners/metadata-scanner.ts
6021
- import { readdir as readdir4, stat as stat6, readFile as readFile10 } from "fs/promises";
6022
- import { join as join8 } from "path";
6288
+ import { readdir as readdir5, stat as stat6, readFile as readFile12 } from "fs/promises";
6289
+ import { join as join9 } from "path";
6023
6290
  var SAFE_LANG = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/;
6024
6291
  var FILE_MAP2 = {
6025
6292
  "title.txt": "title",
@@ -6045,7 +6312,7 @@ var metadataScanner = {
6045
6312
  const findings = [];
6046
6313
  let entries;
6047
6314
  try {
6048
- entries = await readdir4(dir);
6315
+ entries = await readdir5(dir);
6049
6316
  } catch {
6050
6317
  findings.push({
6051
6318
  scanner: "metadata",
@@ -6070,14 +6337,14 @@ var metadataScanner = {
6070
6337
  return findings;
6071
6338
  }
6072
6339
  for (const lang of locales) {
6073
- const langDir = join8(dir, lang);
6340
+ const langDir = join9(dir, lang);
6074
6341
  const langStat = await stat6(langDir).catch(() => null);
6075
6342
  if (!langStat?.isDirectory()) continue;
6076
6343
  const fields = {};
6077
6344
  for (const [fileName, field] of Object.entries(FILE_MAP2)) {
6078
- const filePath = join8(langDir, fileName);
6345
+ const filePath = join9(langDir, fileName);
6079
6346
  try {
6080
- const content = await readFile10(filePath, "utf-8");
6347
+ const content = await readFile12(filePath, "utf-8");
6081
6348
  fields[field] = content.trimEnd();
6082
6349
  } catch {
6083
6350
  }
@@ -6116,9 +6383,9 @@ var metadataScanner = {
6116
6383
  let totalScreenshots = 0;
6117
6384
  let phoneScreenshots = 0;
6118
6385
  for (const ssDir of SCREENSHOT_DIRS) {
6119
- const ssPath = join8(langDir, "images", ssDir);
6386
+ const ssPath = join9(langDir, "images", ssDir);
6120
6387
  try {
6121
- const ssEntries = await readdir4(ssPath);
6388
+ const ssEntries = await readdir5(ssPath);
6122
6389
  const imageFiles = ssEntries.filter((f) => /\.(png|jpe?g|webp)$/i.test(f));
6123
6390
  totalScreenshots += imageFiles.length;
6124
6391
  if (ssDir === "phoneScreenshots") {
@@ -6148,9 +6415,9 @@ var metadataScanner = {
6148
6415
  }
6149
6416
  }
6150
6417
  const defaultLang = locales.includes("en-US") ? "en-US" : locales[0];
6151
- const privacyPath = join8(dir, defaultLang, "privacy_policy_url.txt");
6418
+ const privacyPath = join9(dir, defaultLang, "privacy_policy_url.txt");
6152
6419
  try {
6153
- const url = await readFile10(privacyPath, "utf-8");
6420
+ const url = await readFile12(privacyPath, "utf-8");
6154
6421
  if (!url.trim()) throw new Error("empty");
6155
6422
  } catch {
6156
6423
  findings.push({
@@ -6175,11 +6442,11 @@ var metadataScanner = {
6175
6442
  };
6176
6443
 
6177
6444
  // src/preflight/scanners/secrets-scanner.ts
6178
- import { readFile as readFile11 } from "fs/promises";
6445
+ import { readFile as readFile13 } from "fs/promises";
6179
6446
 
6180
6447
  // src/preflight/scan-files.ts
6181
- import { readdir as readdir5, stat as stat7 } from "fs/promises";
6182
- import { join as join9, extname as extname2 } from "path";
6448
+ import { readdir as readdir6, stat as stat7 } from "fs/promises";
6449
+ import { join as join10, extname as extname3 } from "path";
6183
6450
  var DEFAULT_SKIP_DIRS = /* @__PURE__ */ new Set([
6184
6451
  ".git",
6185
6452
  "node_modules",
@@ -6196,20 +6463,20 @@ async function collectSourceFiles(dir, extensions, skipDirs = DEFAULT_SKIP_DIRS,
6196
6463
  const files = [];
6197
6464
  let entries;
6198
6465
  try {
6199
- entries = await readdir5(dir);
6466
+ entries = await readdir6(dir);
6200
6467
  } catch {
6201
6468
  return files;
6202
6469
  }
6203
6470
  for (const entry of entries) {
6204
6471
  if (skipDirs.has(entry)) continue;
6205
- const fullPath = join9(dir, entry);
6472
+ const fullPath = join10(dir, entry);
6206
6473
  const s = await stat7(fullPath).catch(() => null);
6207
6474
  if (!s) continue;
6208
6475
  if (s.isDirectory()) {
6209
6476
  const sub = await collectSourceFiles(fullPath, extensions, skipDirs, maxDepth - 1);
6210
6477
  files.push(...sub);
6211
6478
  } else if (s.isFile()) {
6212
- const ext = extname2(entry).toLowerCase();
6479
+ const ext = extname3(entry).toLowerCase();
6213
6480
  if (extensions.has(ext) || entry.endsWith(".gradle.kts")) {
6214
6481
  files.push(fullPath);
6215
6482
  }
@@ -6295,7 +6562,7 @@ var secretsScanner = {
6295
6562
  for (const filePath of files) {
6296
6563
  let content;
6297
6564
  try {
6298
- content = await readFile11(filePath, "utf-8");
6565
+ content = await readFile13(filePath, "utf-8");
6299
6566
  } catch {
6300
6567
  continue;
6301
6568
  }
@@ -6323,7 +6590,7 @@ var secretsScanner = {
6323
6590
  };
6324
6591
 
6325
6592
  // src/preflight/scanners/billing-scanner.ts
6326
- import { readFile as readFile12 } from "fs/promises";
6593
+ import { readFile as readFile14 } from "fs/promises";
6327
6594
  var BILLING_PATTERNS = [
6328
6595
  {
6329
6596
  ruleId: "billing-stripe-sdk",
@@ -6384,7 +6651,7 @@ var billingScanner = {
6384
6651
  for (const filePath of files) {
6385
6652
  let content;
6386
6653
  try {
6387
- content = await readFile12(filePath, "utf-8");
6654
+ content = await readFile14(filePath, "utf-8");
6388
6655
  } catch {
6389
6656
  continue;
6390
6657
  }
@@ -6410,7 +6677,7 @@ var billingScanner = {
6410
6677
  };
6411
6678
 
6412
6679
  // src/preflight/scanners/privacy-scanner.ts
6413
- import { readFile as readFile13 } from "fs/promises";
6680
+ import { readFile as readFile15 } from "fs/promises";
6414
6681
  var TRACKING_SDKS = [
6415
6682
  {
6416
6683
  name: "Facebook SDK",
@@ -6450,7 +6717,7 @@ var privacyScanner = {
6450
6717
  for (const filePath of files) {
6451
6718
  let content;
6452
6719
  try {
6453
- content = await readFile13(filePath, "utf-8");
6720
+ content = await readFile15(filePath, "utf-8");
6454
6721
  } catch {
6455
6722
  continue;
6456
6723
  }
@@ -6591,6 +6858,15 @@ var policyScanner = {
6591
6858
  policyUrl: "https://support.google.com/googleplay/android-developer/answer/9876937"
6592
6859
  });
6593
6860
  }
6861
+ findings.push({
6862
+ scanner: "policy",
6863
+ ruleId: "policy-developer-verification",
6864
+ severity: "info",
6865
+ title: "Developer verification enforcement (Sep 30, 2026)",
6866
+ message: "Google begins enforcing developer verification on September 30, 2026, first in Brazil, Indonesia, Singapore, and Thailand. Apps distributed in those markets need a verified developer identity and a registered package name. Apps using Play App Signing are auto-registered.",
6867
+ suggestion: "Run `gpc verify checklist` to review readiness. This is a distribution-market reminder, not an AAB/APK defect.",
6868
+ policyUrl: "https://developer.android.com/developer-verification"
6869
+ });
6594
6870
  if (perms.has("android.permission.SYSTEM_ALERT_WINDOW")) {
6595
6871
  findings.push({
6596
6872
  scanner: "policy",
@@ -6801,57 +7077,17 @@ async function runPreflight(options) {
6801
7077
  };
6802
7078
  }
6803
7079
 
6804
- // src/utils/sort.ts
6805
- function getNestedValue(obj, path) {
6806
- const parts = path.split(".");
6807
- let current = obj;
6808
- for (const part of parts) {
6809
- if (current === null || current === void 0 || typeof current !== "object") {
6810
- return void 0;
6811
- }
6812
- current = current[part];
6813
- }
6814
- return current;
6815
- }
6816
- function compare(a, b) {
6817
- if (a === void 0 && b === void 0) return 0;
6818
- if (a === void 0) return 1;
6819
- if (b === void 0) return -1;
6820
- if (typeof a === "number" && typeof b === "number") {
6821
- return a - b;
6822
- }
6823
- return String(a).localeCompare(String(b));
6824
- }
6825
- function sortResults(items, sortSpec) {
6826
- if (!sortSpec || items.length === 0) {
6827
- return items;
6828
- }
6829
- const descending = sortSpec.startsWith("-");
6830
- const field = descending ? sortSpec.slice(1) : sortSpec;
6831
- const hasField = items.some((item) => getNestedValue(item, field) !== void 0);
6832
- if (!hasField) {
6833
- return items;
6834
- }
6835
- const sorted = [...items].sort((a, b) => {
6836
- const aVal = getNestedValue(a, field);
6837
- const bVal = getNestedValue(b, field);
6838
- const result = compare(aVal, bVal);
6839
- return descending ? -result : result;
6840
- });
6841
- return sorted;
6842
- }
6843
-
6844
7080
  // src/commands/plugin-scaffold.ts
6845
- import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
6846
- import { join as join10 } from "path";
7081
+ import { mkdir as mkdir8, writeFile as writeFile8 } from "fs/promises";
7082
+ import { join as join11 } from "path";
6847
7083
  async function scaffoldPlugin(options) {
6848
7084
  const { name, dir, description = `GPC plugin: ${name}` } = options;
6849
7085
  const pluginName = name.startsWith("gpc-plugin-") ? name : `gpc-plugin-${name}`;
6850
7086
  const shortName = pluginName.replace(/^gpc-plugin-/, "");
6851
- const srcDir = join10(dir, "src");
6852
- const testDir = join10(dir, "tests");
6853
- await mkdir7(srcDir, { recursive: true });
6854
- await mkdir7(testDir, { recursive: true });
7087
+ const srcDir = join11(dir, "src");
7088
+ const testDir = join11(dir, "tests");
7089
+ await mkdir8(srcDir, { recursive: true });
7090
+ await mkdir8(testDir, { recursive: true });
6855
7091
  const files = [];
6856
7092
  const pkg = {
6857
7093
  name: pluginName,
@@ -6885,7 +7121,7 @@ async function scaffoldPlugin(options) {
6885
7121
  vitest: "^3.0.0"
6886
7122
  }
6887
7123
  };
6888
- await writeFile7(join10(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
7124
+ await writeFile8(join11(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
6889
7125
  files.push("package.json");
6890
7126
  const tsconfig = {
6891
7127
  compilerOptions: {
@@ -6901,7 +7137,7 @@ async function scaffoldPlugin(options) {
6901
7137
  },
6902
7138
  include: ["src"]
6903
7139
  };
6904
- await writeFile7(join10(dir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2) + "\n");
7140
+ await writeFile8(join11(dir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2) + "\n");
6905
7141
  files.push("tsconfig.json");
6906
7142
  const srcContent = `import { definePlugin } from "@gpc-cli/plugin-sdk";
6907
7143
  import type { CommandEvent, CommandResult } from "@gpc-cli/plugin-sdk";
@@ -6934,7 +7170,7 @@ export const plugin = definePlugin({
6934
7170
  },
6935
7171
  });
6936
7172
  `;
6937
- await writeFile7(join10(srcDir, "index.ts"), srcContent);
7173
+ await writeFile8(join11(srcDir, "index.ts"), srcContent);
6938
7174
  files.push("src/index.ts");
6939
7175
  const testContent = `import { describe, it, expect, vi } from "vitest";
6940
7176
  import { plugin } from "../src/index";
@@ -6959,7 +7195,7 @@ describe("${pluginName}", () => {
6959
7195
  });
6960
7196
  });
6961
7197
  `;
6962
- await writeFile7(join10(testDir, "plugin.test.ts"), testContent);
7198
+ await writeFile8(join11(testDir, "plugin.test.ts"), testContent);
6963
7199
  files.push("tests/plugin.test.ts");
6964
7200
  return { dir, files };
6965
7201
  }
@@ -7075,7 +7311,7 @@ async function sendWebhook(config, payload, target) {
7075
7311
  }
7076
7312
 
7077
7313
  // src/commands/internal-sharing.ts
7078
- import { extname as extname3 } from "path";
7314
+ import { extname as extname4 } from "path";
7079
7315
  async function uploadInternalSharing(client, packageName, filePath, fileType) {
7080
7316
  const resolvedType = fileType ?? detectFileType(filePath);
7081
7317
  const validation = await validateUploadFile(filePath);
@@ -7102,7 +7338,7 @@ ${validation.errors.join("\n")}`,
7102
7338
  };
7103
7339
  }
7104
7340
  function detectFileType(filePath) {
7105
- const ext = extname3(filePath).toLowerCase();
7341
+ const ext = extname4(filePath).toLowerCase();
7106
7342
  if (ext === ".aab") return "bundle";
7107
7343
  if (ext === ".apk") return "apk";
7108
7344
  throw new GpcError(
@@ -7114,7 +7350,7 @@ function detectFileType(filePath) {
7114
7350
  }
7115
7351
 
7116
7352
  // src/commands/generated-apks.ts
7117
- import { writeFile as writeFile8 } from "fs/promises";
7353
+ import { writeFile as writeFile9 } from "fs/promises";
7118
7354
  async function listGeneratedApks(client, packageName, versionCode) {
7119
7355
  if (!Number.isInteger(versionCode) || versionCode <= 0) {
7120
7356
  throw new GpcError(
@@ -7145,12 +7381,12 @@ async function downloadGeneratedApk(client, packageName, versionCode, apkId, out
7145
7381
  }
7146
7382
  const buffer = await client.generatedApks.download(packageName, versionCode, apkId);
7147
7383
  const bytes = new Uint8Array(buffer);
7148
- await writeFile8(outputPath, bytes);
7384
+ await writeFile9(outputPath, bytes);
7149
7385
  return { path: outputPath, sizeBytes: bytes.byteLength };
7150
7386
  }
7151
7387
 
7152
7388
  // src/commands/bundle-analysis.ts
7153
- import { readFile as readFile14, stat as stat8 } from "fs/promises";
7389
+ import { readFile as readFile16, stat as stat8 } from "fs/promises";
7154
7390
  var EOCD_SIGNATURE = 101010256;
7155
7391
  var CD_SIGNATURE = 33639248;
7156
7392
  var MODULE_SUBDIRS = /* @__PURE__ */ new Set(["dex", "manifest", "res", "assets", "lib", "resources.pb", "root"]);
@@ -7230,7 +7466,7 @@ async function analyzeBundle(filePath) {
7230
7466
  if (!fileInfo || !fileInfo.isFile()) {
7231
7467
  throw new Error(`File not found: ${filePath}`);
7232
7468
  }
7233
- const buf = await readFile14(filePath);
7469
+ const buf = await readFile16(filePath);
7234
7470
  const cdEntries = parseCentralDirectory(buf);
7235
7471
  const fileType = detectFileType2(filePath);
7236
7472
  const isAab = fileType === "aab";
@@ -7316,7 +7552,7 @@ function topFiles(analysis, n = 20) {
7316
7552
  async function checkBundleSize(analysis, configPath = ".bundlesize.json") {
7317
7553
  let config;
7318
7554
  try {
7319
- const raw = await readFile14(configPath, "utf-8");
7555
+ const raw = await readFile16(configPath, "utf-8");
7320
7556
  config = JSON.parse(raw);
7321
7557
  } catch {
7322
7558
  return { passed: true, violations: [] };
@@ -8677,8 +8913,8 @@ async function sha256File(filePath) {
8677
8913
  }
8678
8914
 
8679
8915
  // src/commands/image-sync.ts
8680
- import { readdir as readdir6 } from "fs/promises";
8681
- import { join as join11, extname as extname4 } from "path";
8916
+ import { readdir as readdir7 } from "fs/promises";
8917
+ import { join as join12, extname as extname5 } from "path";
8682
8918
  import { PlayApiError } from "@gpc-cli/api";
8683
8919
  var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg"]);
8684
8920
  var ALL_IMAGE_TYPES2 = [
@@ -8693,15 +8929,15 @@ var ALL_IMAGE_TYPES2 = [
8693
8929
  ];
8694
8930
  async function scanLocalImages(dir) {
8695
8931
  try {
8696
- const entries = await readdir6(dir, { withFileTypes: true });
8697
- return entries.filter((e) => e.isFile() && IMAGE_EXTENSIONS.has(extname4(e.name).toLowerCase())).map((e) => e.name).sort();
8932
+ const entries = await readdir7(dir, { withFileTypes: true });
8933
+ return entries.filter((e) => e.isFile() && IMAGE_EXTENSIONS.has(extname5(e.name).toLowerCase())).map((e) => e.name).sort();
8698
8934
  } catch {
8699
8935
  return [];
8700
8936
  }
8701
8937
  }
8702
8938
  async function scanLanguages(dir) {
8703
8939
  try {
8704
- const entries = await readdir6(dir, { withFileTypes: true });
8940
+ const entries = await readdir7(dir, { withFileTypes: true });
8705
8941
  return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
8706
8942
  } catch {
8707
8943
  return [];
@@ -8726,7 +8962,7 @@ async function syncImages(client, packageName, dir, options) {
8726
8962
  try {
8727
8963
  for (const language of languages) {
8728
8964
  for (const imageType of imageTypes) {
8729
- const localDir = join11(dir, language, imageType);
8965
+ const localDir = join12(dir, language, imageType);
8730
8966
  const localFiles = await scanLocalImages(localDir);
8731
8967
  let remoteImages;
8732
8968
  try {
@@ -8741,7 +8977,7 @@ async function syncImages(client, packageName, dir, options) {
8741
8977
  const remoteSha256Set = new Set(remoteImages.map((img) => img.sha256.toLowerCase()));
8742
8978
  const localHashes = /* @__PURE__ */ new Map();
8743
8979
  for (const file of localFiles) {
8744
- const hash = await sha256File(join11(localDir, file));
8980
+ const hash = await sha256File(join12(localDir, file));
8745
8981
  localHashes.set(file, hash);
8746
8982
  }
8747
8983
  const localSha256Set = new Set(localHashes.values());
@@ -8769,7 +9005,7 @@ async function syncImages(client, packageName, dir, options) {
8769
9005
  details.push({ language, imageType, file, action: "skip", reason: "sha256 match" });
8770
9006
  } else {
8771
9007
  if (!options?.dryRun) {
8772
- const filePath = join11(localDir, file);
9008
+ const filePath = join12(localDir, file);
8773
9009
  const check = await validateImage(filePath, imageType);
8774
9010
  if (!check.valid) {
8775
9011
  throw new GpcError(
@@ -8928,6 +9164,15 @@ function buildChecklist(input) {
8928
9164
  answers
8929
9165
  )
8930
9166
  );
9167
+ items.push(
9168
+ resolveManualStep(
9169
+ "enforcement-markets",
9170
+ "Verification enforcement timeline reviewed",
9171
+ "Enforcement begins September 30, 2026, first in Brazil, Indonesia, Singapore, and Thailand. Apps distributed there need a verified developer identity and a registered package name. Apps using Play App Signing are auto-registered.",
9172
+ VERIFICATION_PAGE,
9173
+ answers
9174
+ )
9175
+ );
8931
9176
  items.push(
8932
9177
  resolveManualStep(
8933
9178
  "auto-registration-reviewed",
@@ -9004,6 +9249,77 @@ function renderChecklistMarkdown(result, accountEmail) {
9004
9249
  lines.push("");
9005
9250
  return lines.join("\n");
9006
9251
  }
9252
+
9253
+ // src/commands/readiness.ts
9254
+ var STATUS_FRACTION = {
9255
+ pass: 1,
9256
+ warn: 0.5,
9257
+ fail: 0
9258
+ };
9259
+ function gradeFromPercent(percent) {
9260
+ if (percent >= 90) return "A";
9261
+ if (percent >= 80) return "B";
9262
+ if (percent >= 70) return "C";
9263
+ if (percent >= 60) return "D";
9264
+ return "F";
9265
+ }
9266
+ function scoreReadiness(signals) {
9267
+ let earned = 0;
9268
+ let possible = 0;
9269
+ const breakdown = [];
9270
+ const suggestions = [];
9271
+ for (const signal of signals) {
9272
+ if (signal.status === "na") {
9273
+ breakdown.push({
9274
+ key: signal.key,
9275
+ label: signal.label,
9276
+ weight: signal.weight,
9277
+ status: "na",
9278
+ points: 0,
9279
+ detail: signal.detail
9280
+ });
9281
+ continue;
9282
+ }
9283
+ const points = signal.weight * STATUS_FRACTION[signal.status];
9284
+ earned += points;
9285
+ possible += signal.weight;
9286
+ breakdown.push({
9287
+ key: signal.key,
9288
+ label: signal.label,
9289
+ weight: signal.weight,
9290
+ status: signal.status,
9291
+ points,
9292
+ detail: signal.detail
9293
+ });
9294
+ if (signal.status !== "pass" && signal.suggestion) {
9295
+ suggestions.push(signal.suggestion);
9296
+ }
9297
+ }
9298
+ const evaluated = possible > 0;
9299
+ const percent = evaluated ? Math.round(earned / possible * 100) : 0;
9300
+ const grade = evaluated ? gradeFromPercent(percent) : "F";
9301
+ return { grade, percent, evaluated, earned, possible, breakdown, suggestions };
9302
+ }
9303
+ var GRADE_COLOR = {
9304
+ A: "brightgreen",
9305
+ B: "green",
9306
+ C: "yellow",
9307
+ D: "orange",
9308
+ F: "red"
9309
+ };
9310
+ function readinessBadgeColor(grade) {
9311
+ return GRADE_COLOR[grade];
9312
+ }
9313
+ function readinessBadgeUrl(score) {
9314
+ const label = "GPC readiness";
9315
+ const message = score.evaluated ? `${score.grade} (${score.percent}%)` : "not evaluated";
9316
+ const color = score.evaluated ? readinessBadgeColor(score.grade) : "lightgrey";
9317
+ const enc = (segment) => encodeURIComponent(segment.replace(/-/g, "--").replace(/_/g, "__"));
9318
+ return `https://img.shields.io/badge/${enc(label)}-${enc(message)}-${color}`;
9319
+ }
9320
+ function readinessBadgeMarkdown(score) {
9321
+ return `![GPC readiness](${readinessBadgeUrl(score)})`;
9322
+ }
9007
9323
  export {
9008
9324
  ApiError,
9009
9325
  ConfigError,
@@ -9020,6 +9336,7 @@ export {
9020
9336
  PROVIDER_WHITELIST,
9021
9337
  PluginManager,
9022
9338
  RENDERERS,
9339
+ REVIEW_SORT_PRESETS,
9023
9340
  SECTION_ORDER,
9024
9341
  SENSITIVE_ARG_KEYS,
9025
9342
  SENSITIVE_KEYS,
@@ -9166,6 +9483,7 @@ export {
9166
9483
  getVitalsOverview,
9167
9484
  getVitalsRendering,
9168
9485
  getVitalsStartup,
9486
+ gradeFromPercent,
9169
9487
  handleBreach,
9170
9488
  importDataSafety,
9171
9489
  importTestersFromCsv,
@@ -9220,11 +9538,18 @@ export {
9220
9538
  promoteRelease,
9221
9539
  publish,
9222
9540
  publishEnterpriseApp,
9541
+ pullAchievementConfigs,
9542
+ pullLeaderboardConfigs,
9223
9543
  pullListings,
9544
+ pushAchievementConfigs,
9545
+ pushLeaderboardConfigs,
9224
9546
  pushListings,
9225
9547
  readListingsFromDir,
9226
9548
  readReleaseNotesForVersion,
9227
9549
  readReleaseNotesFromDir,
9550
+ readinessBadgeColor,
9551
+ readinessBadgeMarkdown,
9552
+ readinessBadgeUrl,
9228
9553
  redactAuditArgs,
9229
9554
  redactSensitive,
9230
9555
  refundExternalTransaction,
@@ -9252,12 +9577,16 @@ export {
9252
9577
  safePathWithin,
9253
9578
  saveStatusCache,
9254
9579
  scaffoldPlugin,
9580
+ scoreReadiness,
9255
9581
  searchAuditEvents,
9256
9582
  searchVitalsErrors,
9257
9583
  sendNotification,
9258
9584
  sendWebhook,
9585
+ setAchievementIcon,
9586
+ setLeaderboardIcon,
9259
9587
  sha256File,
9260
9588
  sortResults,
9589
+ sortReviews,
9261
9590
  startTrain,
9262
9591
  statusHasBreach,
9263
9592
  syncImages,