@gpc-cli/core 0.9.58 → 0.9.59

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
@@ -4,6 +4,7 @@ import {
4
4
  GpcError,
5
5
  NetworkError,
6
6
  applyReleaseNotes,
7
+ commitWithRescue,
7
8
  createTrack,
8
9
  diffReleases,
9
10
  fetchReleaseNotes,
@@ -14,9 +15,10 @@ import {
14
15
  updateTrackConfig,
15
16
  uploadExternallyHosted,
16
17
  uploadRelease,
18
+ validateAndCommit,
17
19
  validateUploadFile,
18
20
  waitForBundleProcessing
19
- } from "./chunk-QIYAOW6R.js";
21
+ } from "./chunk-IZKB6GBS.js";
20
22
 
21
23
  // src/output.ts
22
24
  import process2 from "process";
@@ -915,10 +917,7 @@ async function updateListing(client, packageName, language, data, commitOptions)
915
917
  const edit = await client.edits.insert(packageName);
916
918
  try {
917
919
  const listing = await client.listings.patch(packageName, edit.id, language, data);
918
- if (!commitOptions?.changesNotSentForReview) {
919
- await client.edits.validate(packageName, edit.id);
920
- }
921
- await client.edits.commit(packageName, edit.id, commitOptions);
920
+ await validateAndCommit(client, packageName, edit.id, commitOptions);
922
921
  return listing;
923
922
  } catch (error) {
924
923
  await client.edits.delete(packageName, edit.id).catch(() => {
@@ -931,10 +930,7 @@ async function deleteListing(client, packageName, language, commitOptions) {
931
930
  const edit = await client.edits.insert(packageName);
932
931
  try {
933
932
  await client.listings.delete(packageName, edit.id, language);
934
- if (!commitOptions?.changesNotSentForReview) {
935
- await client.edits.validate(packageName, edit.id);
936
- }
937
- await client.edits.commit(packageName, edit.id, commitOptions);
933
+ await validateAndCommit(client, packageName, edit.id, commitOptions);
938
934
  } catch (error) {
939
935
  await client.edits.delete(packageName, edit.id).catch(() => {
940
936
  });
@@ -1057,10 +1053,7 @@ ${details}`,
1057
1053
  const { language, ...data } = listing;
1058
1054
  await client.listings.update(packageName, edit.id, language, data);
1059
1055
  }
1060
- if (!options?.commitOptions?.changesNotSentForReview) {
1061
- await client.edits.validate(packageName, edit.id);
1062
- }
1063
- await client.edits.commit(packageName, edit.id, options?.commitOptions);
1056
+ await validateAndCommit(client, packageName, edit.id, options?.commitOptions);
1064
1057
  return {
1065
1058
  updated: localListings.length,
1066
1059
  languages: localListings.map((l) => l.language)
@@ -1101,10 +1094,7 @@ async function uploadImage(client, packageName, language, imageType, filePath, c
1101
1094
  const edit = await client.edits.insert(packageName);
1102
1095
  try {
1103
1096
  const image = await client.images.upload(packageName, edit.id, language, imageType, filePath);
1104
- if (!commitOptions?.changesNotSentForReview) {
1105
- await client.edits.validate(packageName, edit.id);
1106
- }
1107
- await client.edits.commit(packageName, edit.id, commitOptions);
1097
+ await validateAndCommit(client, packageName, edit.id, commitOptions);
1108
1098
  return image;
1109
1099
  } catch (error) {
1110
1100
  await client.edits.delete(packageName, edit.id).catch(() => {
@@ -1117,10 +1107,7 @@ async function deleteImage(client, packageName, language, imageType, imageId, co
1117
1107
  const edit = await client.edits.insert(packageName);
1118
1108
  try {
1119
1109
  await client.images.delete(packageName, edit.id, language, imageType, imageId);
1120
- if (!commitOptions?.changesNotSentForReview) {
1121
- await client.edits.validate(packageName, edit.id);
1122
- }
1123
- await client.edits.commit(packageName, edit.id, commitOptions);
1110
+ await validateAndCommit(client, packageName, edit.id, commitOptions);
1124
1111
  } catch (error) {
1125
1112
  await client.edits.delete(packageName, edit.id).catch(() => {
1126
1113
  });
@@ -1164,7 +1151,7 @@ var ALL_IMAGE_TYPES = [
1164
1151
  ];
1165
1152
  async function exportImages(client, packageName, dir, options) {
1166
1153
  const { mkdir: mkdir8, writeFile: writeFile9 } = await import("fs/promises");
1167
- const { join: join12 } = await import("path");
1154
+ const { join: join13 } = await import("path");
1168
1155
  const edit = await client.edits.insert(packageName);
1169
1156
  try {
1170
1157
  let languages;
@@ -1195,7 +1182,7 @@ async function exportImages(client, packageName, dir, options) {
1195
1182
  const batch = tasks.slice(i, i + concurrency);
1196
1183
  const results = await Promise.all(
1197
1184
  batch.map(async (task) => {
1198
- const dirPath = join12(dir, task.language, task.imageType);
1185
+ const dirPath = join13(dir, task.language, task.imageType);
1199
1186
  await mkdir8(dirPath, { recursive: true });
1200
1187
  const response = await fetch(task.url);
1201
1188
  if (!response.ok) {
@@ -1207,7 +1194,7 @@ async function exportImages(client, packageName, dir, options) {
1207
1194
  );
1208
1195
  }
1209
1196
  const buffer = Buffer.from(await response.arrayBuffer());
1210
- const filePath = join12(dirPath, `${task.index}.png`);
1197
+ const filePath = join13(dirPath, `${task.index}.png`);
1211
1198
  await writeFile9(filePath, buffer);
1212
1199
  return buffer.length;
1213
1200
  })
@@ -1233,10 +1220,7 @@ async function updateAppDetails(client, packageName, details, commitOptions) {
1233
1220
  const edit = await client.edits.insert(packageName);
1234
1221
  try {
1235
1222
  const result = await client.details.patch(packageName, edit.id, details);
1236
- if (!commitOptions?.changesNotSentForReview) {
1237
- await client.edits.validate(packageName, edit.id);
1238
- }
1239
- await client.edits.commit(packageName, edit.id, commitOptions);
1223
+ await validateAndCommit(client, packageName, edit.id, commitOptions);
1240
1224
  return result;
1241
1225
  } catch (error) {
1242
1226
  await client.edits.delete(packageName, edit.id).catch(() => {
@@ -3238,7 +3222,7 @@ async function handleBreach(event, config, client) {
3238
3222
  }
3239
3223
  case "halt": {
3240
3224
  try {
3241
- const { updateRollout: updateRollout2 } = await import("./releases-I5MYFNCV.js");
3225
+ const { updateRollout: updateRollout2 } = await import("./releases-VFDJ6IX2.js");
3242
3226
  await updateRollout2(client, config.packageName, config.track, "halt");
3243
3227
  halted = true;
3244
3228
  } catch {
@@ -3822,10 +3806,7 @@ async function addTesters(client, packageName, track, groupEmails, commitOptions
3822
3806
  const updated = await client.testers.update(packageName, edit.id, track, {
3823
3807
  googleGroups: [...existing]
3824
3808
  });
3825
- if (!commitOptions?.changesNotSentForReview) {
3826
- await client.edits.validate(packageName, edit.id);
3827
- }
3828
- await client.edits.commit(packageName, edit.id, commitOptions);
3809
+ await validateAndCommit(client, packageName, edit.id, commitOptions);
3829
3810
  return updated;
3830
3811
  } catch (error) {
3831
3812
  await client.edits.delete(packageName, edit.id).catch(() => {
@@ -3842,10 +3823,7 @@ async function removeTesters(client, packageName, track, groupEmails, commitOpti
3842
3823
  const updated = await client.testers.update(packageName, edit.id, track, {
3843
3824
  googleGroups: filtered
3844
3825
  });
3845
- if (!commitOptions?.changesNotSentForReview) {
3846
- await client.edits.validate(packageName, edit.id);
3847
- }
3848
- await client.edits.commit(packageName, edit.id, commitOptions);
3826
+ await validateAndCommit(client, packageName, edit.id, commitOptions);
3849
3827
  return updated;
3850
3828
  } catch (error) {
3851
3829
  await client.edits.delete(packageName, edit.id).catch(() => {
@@ -8371,6 +8349,202 @@ function compareFingerprints(a, b) {
8371
8349
  return normalizeFingerprint(a) === normalizeFingerprint(b);
8372
8350
  }
8373
8351
 
8352
+ // src/utils/hash.ts
8353
+ import { createHash } from "crypto";
8354
+ import { stat as stat9 } from "fs/promises";
8355
+ async function sha256File(filePath) {
8356
+ const hash = createHash("sha256");
8357
+ const { size } = await stat9(filePath);
8358
+ if (size === 0) return hash.digest("hex");
8359
+ const { createReadStream } = await import("fs");
8360
+ const stream = createReadStream(filePath);
8361
+ await new Promise((resolve2, reject) => {
8362
+ stream.on("data", (chunk) => hash.update(chunk));
8363
+ stream.on("end", resolve2);
8364
+ stream.on("error", reject);
8365
+ });
8366
+ return hash.digest("hex");
8367
+ }
8368
+
8369
+ // src/commands/image-sync.ts
8370
+ import { readdir as readdir7 } from "fs/promises";
8371
+ import { join as join12, extname as extname5 } from "path";
8372
+ import { PlayApiError } from "@gpc-cli/api";
8373
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg"]);
8374
+ var ALL_IMAGE_TYPES2 = [
8375
+ "icon",
8376
+ "featureGraphic",
8377
+ "tvBanner",
8378
+ "phoneScreenshots",
8379
+ "sevenInchScreenshots",
8380
+ "tenInchScreenshots",
8381
+ "tvScreenshots",
8382
+ "wearScreenshots"
8383
+ ];
8384
+ async function scanLocalImages(dir) {
8385
+ try {
8386
+ const entries = await readdir7(dir, { withFileTypes: true });
8387
+ return entries.filter((e) => e.isFile() && IMAGE_EXTENSIONS.has(extname5(e.name).toLowerCase())).map((e) => e.name).sort();
8388
+ } catch {
8389
+ return [];
8390
+ }
8391
+ }
8392
+ async function scanLanguages(dir) {
8393
+ try {
8394
+ const entries = await readdir7(dir, { withFileTypes: true });
8395
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
8396
+ } catch {
8397
+ return [];
8398
+ }
8399
+ }
8400
+ async function syncImages(client, packageName, dir, options) {
8401
+ const details = [];
8402
+ let uploaded = 0;
8403
+ let skipped = 0;
8404
+ let deleted = 0;
8405
+ const languages = options?.lang ? [options.lang] : await scanLanguages(dir);
8406
+ if (languages.length === 0) {
8407
+ throw new GpcError(
8408
+ `No language directories found in "${dir}"`,
8409
+ "IMAGE_SYNC_EMPTY",
8410
+ 1,
8411
+ "The directory should contain subdirectories named by language code (e.g., en-US/) with image type subdirectories inside."
8412
+ );
8413
+ }
8414
+ const imageTypes = options?.type ? [options.type] : ALL_IMAGE_TYPES2;
8415
+ const edit = await client.edits.insert(packageName);
8416
+ try {
8417
+ for (const language of languages) {
8418
+ for (const imageType of imageTypes) {
8419
+ const localDir = join12(dir, language, imageType);
8420
+ const localFiles = await scanLocalImages(localDir);
8421
+ let remoteImages;
8422
+ try {
8423
+ remoteImages = await client.images.list(packageName, edit.id, language, imageType);
8424
+ } catch (error) {
8425
+ if (error instanceof PlayApiError && error.statusCode === 404) {
8426
+ remoteImages = [];
8427
+ } else {
8428
+ throw error;
8429
+ }
8430
+ }
8431
+ const remoteSha256Set = new Set(remoteImages.map((img) => img.sha256.toLowerCase()));
8432
+ const localHashes = /* @__PURE__ */ new Map();
8433
+ for (const file of localFiles) {
8434
+ const hash = await sha256File(join12(localDir, file));
8435
+ localHashes.set(file, hash);
8436
+ }
8437
+ const localSha256Set = new Set(localHashes.values());
8438
+ if (options?.delete) {
8439
+ for (const img of remoteImages) {
8440
+ if (!localSha256Set.has(img.sha256.toLowerCase())) {
8441
+ if (!options?.dryRun) {
8442
+ await client.images.delete(packageName, edit.id, language, imageType, img.id);
8443
+ }
8444
+ deleted++;
8445
+ details.push({
8446
+ language,
8447
+ imageType,
8448
+ file: img.id,
8449
+ action: "delete",
8450
+ reason: "not in local"
8451
+ });
8452
+ }
8453
+ }
8454
+ }
8455
+ for (const file of localFiles) {
8456
+ const hash = localHashes.get(file);
8457
+ if (remoteSha256Set.has(hash)) {
8458
+ skipped++;
8459
+ details.push({ language, imageType, file, action: "skip", reason: "sha256 match" });
8460
+ } else {
8461
+ if (!options?.dryRun) {
8462
+ const filePath = join12(localDir, file);
8463
+ const check = await validateImage(filePath, imageType);
8464
+ if (!check.valid) {
8465
+ throw new GpcError(
8466
+ `Image validation failed for ${language}/${imageType}/${file}: ${check.errors.join("; ")}`,
8467
+ "IMAGE_INVALID",
8468
+ 2,
8469
+ "Check image dimensions, file size, and format."
8470
+ );
8471
+ }
8472
+ await client.images.upload(packageName, edit.id, language, imageType, filePath);
8473
+ }
8474
+ uploaded++;
8475
+ details.push({ language, imageType, file, action: "upload", reason: "new or changed" });
8476
+ }
8477
+ }
8478
+ }
8479
+ }
8480
+ if (options?.dryRun || uploaded === 0 && deleted === 0) {
8481
+ await client.edits.delete(packageName, edit.id).catch(() => {
8482
+ });
8483
+ } else {
8484
+ await validateAndCommit(client, packageName, edit.id, options?.commitOptions);
8485
+ }
8486
+ return {
8487
+ uploaded,
8488
+ skipped,
8489
+ deleted,
8490
+ total: uploaded + skipped + deleted,
8491
+ details
8492
+ };
8493
+ } catch (error) {
8494
+ await client.edits.delete(packageName, edit.id).catch(() => {
8495
+ });
8496
+ throw error;
8497
+ }
8498
+ }
8499
+
8500
+ // src/commands/bundles.ts
8501
+ import { PlayApiError as PlayApiError2 } from "@gpc-cli/api";
8502
+ async function listBundles(client, packageName) {
8503
+ const edit = await client.edits.insert(packageName);
8504
+ try {
8505
+ const bundles = await client.bundles.list(packageName, edit.id);
8506
+ await client.edits.delete(packageName, edit.id).catch(() => {
8507
+ });
8508
+ return bundles;
8509
+ } catch (error) {
8510
+ await client.edits.delete(packageName, edit.id).catch(() => {
8511
+ });
8512
+ throw error;
8513
+ }
8514
+ }
8515
+ async function findBundle(client, packageName, versionCode) {
8516
+ const bundles = await listBundles(client, packageName);
8517
+ return bundles.find((b) => b.versionCode === versionCode) ?? null;
8518
+ }
8519
+ function isRetryableError(error) {
8520
+ if (!(error instanceof PlayApiError2)) return false;
8521
+ const status = error.statusCode;
8522
+ return status === 429 || status === 500 || status === 503 || status === 409;
8523
+ }
8524
+ async function waitForBundle(client, packageName, versionCode, options) {
8525
+ const timeout = options?.timeout ?? 6e5;
8526
+ const interval = options?.interval ?? 15e3;
8527
+ const deadline = Date.now() + timeout;
8528
+ while (Date.now() < deadline) {
8529
+ try {
8530
+ const bundles = await listBundles(client, packageName);
8531
+ const match = bundles.find((b) => b.versionCode === versionCode);
8532
+ if (match) return match;
8533
+ } catch (error) {
8534
+ if (!isRetryableError(error)) throw error;
8535
+ }
8536
+ const remaining = deadline - Date.now();
8537
+ if (remaining <= 0) break;
8538
+ await new Promise((r) => setTimeout(r, Math.min(interval, remaining)));
8539
+ }
8540
+ throw new GpcError(
8541
+ `Bundle version code ${versionCode} not found after ${Math.round(timeout / 1e3)}s`,
8542
+ "BUNDLE_WAIT_TIMEOUT",
8543
+ 4,
8544
+ "The bundle may still be processing. Try again with a longer --timeout, or check the Play Console."
8545
+ );
8546
+ }
8547
+
8374
8548
  // src/signing-consistency.ts
8375
8549
  async function checkSigningConsistency(accessToken, packageName, apiHost = "androidpublisher.googleapis.com") {
8376
8550
  const baseUrl = `https://${apiHost}/androidpublisher/v3/applications/${encodeURIComponent(packageName)}`;
@@ -8613,6 +8787,7 @@ export {
8613
8787
  checkThreshold,
8614
8788
  classifyError,
8615
8789
  clearAuditLog,
8790
+ commitWithRescue,
8616
8791
  compareBundles,
8617
8792
  compareFingerprints,
8618
8793
  compareVersionVitals,
@@ -8667,6 +8842,7 @@ export {
8667
8842
  fetchAggregateCost,
8668
8843
  fetchChangelog,
8669
8844
  fetchReleaseNotes,
8845
+ findBundle,
8670
8846
  formatChangelogEntry,
8671
8847
  formatCustomPayload,
8672
8848
  formatDiscordPayload,
@@ -8733,6 +8909,7 @@ export {
8733
8909
  lintLocalListings,
8734
8910
  listAchievements,
8735
8911
  listAuditEvents,
8912
+ listBundles,
8736
8913
  listDeviceTiers,
8737
8914
  listEvents,
8738
8915
  listGeneratedApks,
@@ -8801,9 +8978,11 @@ export {
8801
8978
  searchVitalsErrors,
8802
8979
  sendNotification,
8803
8980
  sendWebhook,
8981
+ sha256File,
8804
8982
  sortResults,
8805
8983
  startTrain,
8806
8984
  statusHasBreach,
8985
+ syncImages,
8807
8986
  syncInAppProducts,
8808
8987
  topFiles,
8809
8988
  trackBreachState,
@@ -8824,6 +9003,7 @@ export {
8824
9003
  uploadImage,
8825
9004
  uploadInternalSharing,
8826
9005
  uploadRelease,
9006
+ validateAndCommit,
8827
9007
  validateBundleForApply,
8828
9008
  validateImage,
8829
9009
  validateLanguageCode,
@@ -8834,6 +9014,7 @@ export {
8834
9014
  validateTrackName,
8835
9015
  validateUploadFile,
8836
9016
  validateVersionCode,
9017
+ waitForBundle,
8837
9018
  waitForBundleProcessing,
8838
9019
  watchVitalsWithAutoHalt,
8839
9020
  wordDiff,