pabal-store-api-mcp 1.3.11 → 1.3.14

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.
@@ -10,6 +10,10 @@ export declare function resolveGooglePlayLocales(allLocales: string[], requested
10
10
  localesToPush: string[];
11
11
  missingLocales: string[];
12
12
  };
13
+ export declare function shouldPushGooglePlayAppDetails({ hasContactDetails, requestedLocales, }: {
14
+ hasContactDetails: boolean;
15
+ requestedLocales?: string[];
16
+ }): boolean;
13
17
  /**
14
18
  * Google Play-facing service layer that wraps client creation and common operations.
15
19
  * Keeps MCP tools independent from client factories and SDK details.
@@ -6,6 +6,7 @@ import { verifyPlayStoreAuth } from "../../packages/stores/play-store/verify-aut
6
6
  import { createGooglePlayClient } from "../../core/clients/google-play-factory.js";
7
7
  import { parseGooglePlayScreenshots, hasScreenshots, } from "../../core/helpers/screenshot-helpers.js";
8
8
  import { checkPushPrerequisites, serviceFailure, toServiceResult, updateRegisteredLocales, } from "./service-helpers.js";
9
+ const GOOGLE_PLAY_SCREENSHOT_LOCALE_BATCH_SIZE = 5;
9
10
  export function resolveGooglePlayLocales(allLocales, requestedLocales) {
10
11
  if (!requestedLocales?.length) {
11
12
  return { localesToPush: allLocales, missingLocales: [] };
@@ -16,6 +17,9 @@ export function resolveGooglePlayLocales(allLocales, requestedLocales) {
16
17
  missingLocales: requestedLocales.filter((locale) => !allLocales.includes(locale)),
17
18
  };
18
19
  }
20
+ export function shouldPushGooglePlayAppDetails({ hasContactDetails, requestedLocales, }) {
21
+ return hasContactDetails && !requestedLocales?.length;
22
+ }
19
23
  /**
20
24
  * Google Play-facing service layer that wraps client creation and common operations.
21
25
  * Keeps MCP tools independent from client factories and SDK details.
@@ -197,8 +201,13 @@ export class GooglePlayService {
197
201
  googlePlayData.locales[locale],
198
202
  ])),
199
203
  });
200
- // Push app-level contact information
201
- if (googlePlayData.contactEmail || googlePlayData.contactWebsite) {
204
+ // Push app-level contact information once for full pushes. Partial locale
205
+ // pushes are commonly batched, and repeating details edits can invalidate
206
+ // otherwise-successful listing commits on Google Play.
207
+ if (shouldPushGooglePlayAppDetails({
208
+ hasContactDetails: Boolean(googlePlayData.contactEmail || googlePlayData.contactWebsite),
209
+ requestedLocales: locales,
210
+ })) {
202
211
  console.error(`[GooglePlay] 📤 Pushing app details...`);
203
212
  await client.pushAppDetails({
204
213
  contactEmail: googlePlayData.contactEmail,
@@ -206,6 +215,9 @@ export class GooglePlayService {
206
215
  });
207
216
  console.error(`[GooglePlay] ✅ App details uploaded successfully`);
208
217
  }
218
+ else if (locales?.length) {
219
+ console.error(`[GooglePlay] ⏭️ Skipping app details for partial locale push`);
220
+ }
209
221
  // Upload screenshots if enabled
210
222
  if (uploadImages && slug) {
211
223
  console.error(`[GooglePlay] 📤 Uploading screenshots...`);
@@ -214,6 +226,7 @@ export class GooglePlayService {
214
226
  const uploadedLocales = [];
215
227
  const skippedLocales = [];
216
228
  const failedLocales = [];
229
+ const screenshotUploadOptions = [];
217
230
  for (const locale of localesToPush) {
218
231
  try {
219
232
  const localeData = googlePlayData.locales[locale];
@@ -271,11 +284,11 @@ export class GooglePlayService {
271
284
  skippedLocales.push(locale);
272
285
  continue;
273
286
  }
274
- console.error(`[GooglePlay] 📤 Uploading screenshots for ${locale} (batch mode - will replace existing)...`);
287
+ console.error(`[GooglePlay] 📋 Queued screenshots for ${locale} (batch mode - will replace existing)...`);
275
288
  // Google Play upload strategy:
276
289
  // - phone → uploads to phoneScreenshots AND sevenInchScreenshots (both use same images)
277
290
  // - tablet → uploads to tenInchScreenshots only
278
- const uploadResult = await client.uploadScreenshotsForLocale({
291
+ screenshotUploadOptions.push({
279
292
  language: locale,
280
293
  phoneScreenshots: screenshots.phone,
281
294
  sevenInchScreenshots: screenshots.phone,
@@ -283,14 +296,28 @@ export class GooglePlayService {
283
296
  featureGraphic: screenshots.featureGraphic || undefined,
284
297
  imageUploadTimeoutMs,
285
298
  });
286
- console.error(`[GooglePlay] ✅ Images uploaded for ${locale}: ${uploadResult.uploaded.phoneScreenshots} phone, ${uploadResult.uploaded.sevenInchScreenshots} 7-inch, ${uploadResult.uploaded.tenInchScreenshots} 10-inch, feature graphic ${uploadResult.uploaded.featureGraphic ? "yes" : "no"}`);
287
- uploadedLocales.push(locale);
288
299
  }
289
300
  catch (error) {
290
301
  console.error(`[GooglePlay] ❌ Failed to upload screenshots for ${locale}: ${error instanceof Error ? error.message : String(error)}`);
291
302
  failedLocales.push(locale);
292
303
  }
293
304
  }
305
+ for (let offset = 0; offset < screenshotUploadOptions.length; offset += GOOGLE_PLAY_SCREENSHOT_LOCALE_BATCH_SIZE) {
306
+ const batch = screenshotUploadOptions.slice(offset, offset + GOOGLE_PLAY_SCREENSHOT_LOCALE_BATCH_SIZE);
307
+ try {
308
+ console.error(`[GooglePlay] 📤 Uploading screenshots for ${batch.length} locale(s) in one edit...`);
309
+ const uploadResults = await client.uploadScreenshotsForLocales(batch);
310
+ for (const uploadResult of uploadResults) {
311
+ console.error(`[GooglePlay] ✅ Images uploaded for ${uploadResult.language}: ${uploadResult.uploaded.phoneScreenshots} phone, ${uploadResult.uploaded.sevenInchScreenshots} 7-inch, ${uploadResult.uploaded.tenInchScreenshots} 10-inch, feature graphic ${uploadResult.uploaded.featureGraphic ? "yes" : "no"}`);
312
+ uploadedLocales.push(uploadResult.language);
313
+ }
314
+ }
315
+ catch (error) {
316
+ const failedBatchLocales = batch.map((option) => option.language);
317
+ console.error(`[GooglePlay] ❌ Batch screenshot upload failed for ${failedBatchLocales.join(", ")}: ${error instanceof Error ? error.message : String(error)}`);
318
+ failedLocales.push(...failedBatchLocales);
319
+ }
320
+ }
294
321
  console.error(`[GooglePlay] 📊 Screenshot upload summary: ${uploadedLocales.length} succeeded, ${skippedLocales.length} skipped, ${failedLocales.length} failed`);
295
322
  if (uploadedLocales.length > 0) {
296
323
  console.error(`[GooglePlay] ✅ Uploaded: ${uploadedLocales.join(", ")}`);
@@ -299,7 +326,9 @@ export class GooglePlayService {
299
326
  console.error(`[GooglePlay] ⏭️ Skipped: ${skippedLocales.join(", ")}`);
300
327
  }
301
328
  if (failedLocales.length > 0) {
302
- console.error(`[GooglePlay] ❌ Failed: ${failedLocales.join(", ")}`);
329
+ const uniqueFailedLocales = [...new Set(failedLocales)];
330
+ console.error(`[GooglePlay] ❌ Failed: ${uniqueFailedLocales.join(", ")}`);
331
+ throw new Error(`Screenshot upload failed for locales: ${uniqueFailedLocales.join(", ")}`);
303
332
  }
304
333
  }
305
334
  try {
package/dist/src/index.js CHANGED
@@ -204,7 +204,7 @@ registerToolWithInfo("aso-push", {
204
204
  .int()
205
205
  .positive()
206
206
  .optional()
207
- .describe("Per-image upload timeout in milliseconds"),
207
+ .describe("Per-image upload timeout in milliseconds (default: 600000 when uploadImages=true)"),
208
208
  dryRun: z
209
209
  .boolean()
210
210
  .optional()
@@ -108,15 +108,34 @@ export declare class AppStoreClient {
108
108
  * Delete all screenshots in a screenshot set
109
109
  */
110
110
  private deleteAllScreenshotsInSet;
111
+ private deleteScreenshots;
112
+ private prepareScreenshotSetForUpload;
111
113
  private sanitizePathSegment;
112
114
  private getScreenshotLockPath;
113
115
  private acquireScreenshotUploadLock;
114
116
  private preflightScreenshotBatch;
115
117
  /**
116
- * Upload multiple screenshots for a locale, replacing existing ones
118
+ * Upload multiple screenshots for a locale, replacing existing ones.
119
+ *
120
+ * Failure model:
121
+ *
122
+ * existing screenshots ──┐
123
+ * ├─ free only the slots App Store requires
124
+ * incoming screenshots ──┘
125
+ * │
126
+ * ├─ upload + commit every incoming screenshot
127
+ * │
128
+ * └─ only after success, delete the remaining old screenshots
129
+ *
130
+ * This avoids the old delete-first behavior where a timeout could leave the
131
+ * user with an empty screenshot set. If upload fails, most existing
132
+ * screenshots remain visible and any successfully uploaded new screenshots
133
+ * are left in App Store Connect for manual or retry cleanup.
134
+ *
117
135
  * 1. Find or create screenshot sets for each display type
118
- * 2. Delete existing screenshots in each set
136
+ * 2. Delete only enough existing screenshots to fit the incoming batch
119
137
  * 3. Upload new screenshots in order
138
+ * 4. Delete remaining old screenshots after the batch succeeds
120
139
  */
121
140
  uploadScreenshotsForLocale(options: {
122
141
  locale: string;
@@ -12,6 +12,9 @@ import { DEFAULT_LOCALE } from "../../../packages/configs/aso-config/constants.j
12
12
  import { getAsoDir } from "../../../packages/configs/aso-config/utils.js";
13
13
  import { convertToAsoData, convertToMultilingualAsoData, convertToReleaseNote, fetchScreenshotsForLocalization, mapLocalizationsByLocale, selectEnglishAppName, sortReleaseNotes, sortVersions, } from "./api-converters.js";
14
14
  import { APP_STORE_API_BASE_URL, APP_STORE_PLATFORM, DEFAULT_APP_LIST_LIMIT, DEFAULT_VERSIONS_FETCH_LIMIT, } from "./constants.js";
15
+ const APP_STORE_SCREENSHOT_SET_MAX_COUNT = 10;
16
+ const DEFAULT_IMAGE_UPLOAD_TIMEOUT_MS = 10 * 60 * 1000;
17
+ const SCREENSHOT_UPLOAD_LOCK_STALE_MS = 30 * 60 * 1000;
15
18
  export class AppStoreClient {
16
19
  issuerId;
17
20
  keyId;
@@ -727,9 +730,10 @@ export class AppStoreClient {
727
730
  }
728
731
  });
729
732
  req.on("error", reject);
730
- if (timeoutMs) {
731
- req.setTimeout(timeoutMs, () => {
732
- req.destroy(new Error(`Upload timed out after ${timeoutMs}ms`));
733
+ const effectiveTimeoutMs = timeoutMs ?? DEFAULT_IMAGE_UPLOAD_TIMEOUT_MS;
734
+ if (effectiveTimeoutMs > 0) {
735
+ req.setTimeout(effectiveTimeoutMs, () => {
736
+ req.destroy(new Error(`Upload timed out after ${effectiveTimeoutMs}ms`));
733
737
  });
734
738
  }
735
739
  req.write(fileBuffer);
@@ -779,13 +783,47 @@ export class AppStoreClient {
779
783
  async deleteAllScreenshotsInSet(screenshotSetId) {
780
784
  const screenshotsResponse = await this.listScreenshots(screenshotSetId);
781
785
  const screenshots = screenshotsResponse.data || [];
786
+ return this.deleteScreenshots(screenshots);
787
+ }
788
+ async deleteScreenshots(screenshots) {
782
789
  let deletedCount = 0;
783
790
  for (const screenshot of screenshots) {
791
+ if (screenshot.type !== "appScreenshots") {
792
+ console.error(`[AppStore] Skipping non-screenshot asset ${screenshot.id}`);
793
+ continue;
794
+ }
784
795
  await this.deleteScreenshot(screenshot.id);
785
796
  deletedCount++;
786
797
  }
787
798
  return deletedCount;
788
799
  }
800
+ async prepareScreenshotSetForUpload(screenshotSetId, incomingCount) {
801
+ if (incomingCount > APP_STORE_SCREENSHOT_SET_MAX_COUNT) {
802
+ throw new Error(`Preflight failed: App Store allows up to ${APP_STORE_SCREENSHOT_SET_MAX_COUNT} screenshots per display type, got ${incomingCount}`);
803
+ }
804
+ const screenshotsResponse = await this.listScreenshots(screenshotSetId);
805
+ const existingScreenshots = (screenshotsResponse.data || []).filter((screenshot) => screenshot.type === "appScreenshots");
806
+ const nonScreenshotCount = (screenshotsResponse.data || []).length - existingScreenshots.length;
807
+ if (nonScreenshotCount > 0) {
808
+ console.error(`[AppStore] Ignoring ${nonScreenshotCount} non-screenshot asset(s) in screenshot set`);
809
+ }
810
+ const slotsToFree = Math.max(0, existingScreenshots.length +
811
+ incomingCount -
812
+ APP_STORE_SCREENSHOT_SET_MAX_COUNT);
813
+ // Preserve the first screenshots as long as possible. If the upload fails
814
+ // after freeing slots, users are more likely to still see the primary
815
+ // screenshots rather than the tail of the old set.
816
+ const screenshotsToDeleteBeforeUpload = slotsToFree > 0 ? existingScreenshots.slice(-slotsToFree) : [];
817
+ const screenshotsToDeleteAfterUpload = existingScreenshots.slice(0, existingScreenshots.length - slotsToFree);
818
+ const deletedBeforeUpload = await this.deleteScreenshots(screenshotsToDeleteBeforeUpload);
819
+ if (deletedBeforeUpload > 0) {
820
+ console.error(`[AppStore] Deleted ${deletedBeforeUpload} existing screenshots to free upload slots`);
821
+ }
822
+ return {
823
+ deletedBeforeUpload,
824
+ screenshotsToDeleteAfterUpload,
825
+ };
826
+ }
789
827
  sanitizePathSegment(value) {
790
828
  return value.replace(/[^a-zA-Z0-9._-]/g, "_");
791
829
  }
@@ -804,9 +842,19 @@ export class AppStoreClient {
804
842
  catch (error) {
805
843
  const code = error.code;
806
844
  if (code === "EEXIST") {
807
- throw new Error(`Screenshot upload lock is already held (${lockPath}). Another upload is running for this locale/display type.`);
845
+ const lockAgeMs = Date.now() - statSync(lockPath).mtimeMs;
846
+ if (lockAgeMs > SCREENSHOT_UPLOAD_LOCK_STALE_MS) {
847
+ console.error(`[AppStore] Removing stale screenshot upload lock (${lockPath})`);
848
+ unlinkSync(lockPath);
849
+ descriptor = openSync(lockPath, "wx");
850
+ }
851
+ else {
852
+ throw new Error(`Screenshot upload lock is already held (${lockPath}). Another upload is running for this locale/display type.`);
853
+ }
854
+ }
855
+ else {
856
+ throw error;
808
857
  }
809
- throw error;
810
858
  }
811
859
  closeSync(descriptor);
812
860
  return () => {
@@ -852,10 +900,27 @@ export class AppStoreClient {
852
900
  }
853
901
  }
854
902
  /**
855
- * Upload multiple screenshots for a locale, replacing existing ones
903
+ * Upload multiple screenshots for a locale, replacing existing ones.
904
+ *
905
+ * Failure model:
906
+ *
907
+ * existing screenshots ──┐
908
+ * ├─ free only the slots App Store requires
909
+ * incoming screenshots ──┘
910
+ * │
911
+ * ├─ upload + commit every incoming screenshot
912
+ * │
913
+ * └─ only after success, delete the remaining old screenshots
914
+ *
915
+ * This avoids the old delete-first behavior where a timeout could leave the
916
+ * user with an empty screenshot set. If upload fails, most existing
917
+ * screenshots remain visible and any successfully uploaded new screenshots
918
+ * are left in App Store Connect for manual or retry cleanup.
919
+ *
856
920
  * 1. Find or create screenshot sets for each display type
857
- * 2. Delete existing screenshots in each set
921
+ * 2. Delete only enough existing screenshots to fit the incoming batch
858
922
  * 3. Upload new screenshots in order
923
+ * 4. Delete remaining old screenshots after the batch succeeds
859
924
  */
860
925
  async uploadScreenshotsForLocale(options) {
861
926
  const { locale, screenshots, imageUploadTimeoutMs } = options;
@@ -902,13 +967,10 @@ export class AppStoreClient {
902
967
  console.error(`[AppStore] Processing ${displayType} (${screenshotList.length} screenshots)...`);
903
968
  // Find or create screenshot set
904
969
  const screenshotSetId = await this.findOrCreateScreenshotSet(localizationId, displayType);
905
- // Delete existing screenshots in this set
906
- const deletedCount = await this.deleteAllScreenshotsInSet(screenshotSetId);
907
- if (deletedCount > 0) {
908
- console.error(`[AppStore] Deleted ${deletedCount} existing screenshots`);
909
- result.deleted += deletedCount;
910
- }
970
+ const { deletedBeforeUpload, screenshotsToDeleteAfterUpload } = await this.prepareScreenshotSetForUpload(screenshotSetId, screenshotList.length);
971
+ result.deleted += deletedBeforeUpload;
911
972
  // Upload new screenshots in order
973
+ let uploadedForDisplayType = 0;
912
974
  for (const screenshot of screenshotList) {
913
975
  try {
914
976
  const fileBuffer = readFileSync(screenshot.path);
@@ -925,12 +987,20 @@ export class AppStoreClient {
925
987
  await this.commitAppScreenshot(screenshotData.id);
926
988
  console.error(`[AppStore] ✅ ${screenshot.filename}`);
927
989
  result.uploaded++;
990
+ uploadedForDisplayType++;
928
991
  }
929
992
  catch (error) {
930
993
  result.failed++;
931
994
  throw new Error(`[${displayType}] ${screenshot.filename}: ${error instanceof Error ? error.message : String(error)}`);
932
995
  }
933
996
  }
997
+ if (uploadedForDisplayType === screenshotList.length) {
998
+ const deletedAfterUpload = await this.deleteScreenshots(screenshotsToDeleteAfterUpload);
999
+ if (deletedAfterUpload > 0) {
1000
+ console.error(`[AppStore] Deleted ${deletedAfterUpload} replaced screenshots after successful upload`);
1001
+ result.deleted += deletedAfterUpload;
1002
+ }
1003
+ }
934
1004
  }
935
1005
  finally {
936
1006
  releaseLock();
@@ -55,6 +55,8 @@ export declare class GooglePlayClient {
55
55
  * Deletes existing screenshots before uploading new ones
56
56
  */
57
57
  uploadScreenshotsForLocale(options: BatchUploadScreenshotsOptions): Promise<BatchUploadScreenshotsResult>;
58
+ uploadScreenshotsForLocales(optionsList: BatchUploadScreenshotsOptions[]): Promise<BatchUploadScreenshotsResult[]>;
59
+ private uploadScreenshotsInSession;
58
60
  private uploadImageWithOptionalTimeout;
59
61
  private getTrack;
60
62
  private updateTrack;
@@ -620,7 +620,6 @@ export class GooglePlayClient {
620
620
  * Deletes existing screenshots before uploading new ones
621
621
  */
622
622
  async uploadScreenshotsForLocale(options) {
623
- const { language, phoneScreenshots = [], sevenInchScreenshots = [], tenInchScreenshots = [], featureGraphic, imageUploadTimeoutMs, } = options;
624
623
  const authClient = await this.auth.getClient();
625
624
  const editResponse = await this.createEdit(authClient, this.packageName);
626
625
  const editId = editResponse.data.id;
@@ -629,6 +628,61 @@ export class GooglePlayClient {
629
628
  packageName: this.packageName,
630
629
  editId,
631
630
  };
631
+ try {
632
+ const result = await this.uploadScreenshotsInSession(session, options);
633
+ // Commit all changes
634
+ console.error(`[GooglePlayClient] Committing screenshots for ${options.language}...`);
635
+ await this.commitEdit(session);
636
+ console.error(`[GooglePlayClient] ✅ Screenshots committed for ${options.language}`);
637
+ return result;
638
+ }
639
+ catch (error) {
640
+ console.error(`[GooglePlayClient] Rolling back screenshot upload for ${options.language}...`);
641
+ try {
642
+ await this.deleteEdit(session);
643
+ }
644
+ catch {
645
+ // Ignore deletion failure
646
+ }
647
+ throw error;
648
+ }
649
+ }
650
+ async uploadScreenshotsForLocales(optionsList) {
651
+ if (optionsList.length === 0)
652
+ return [];
653
+ const authClient = await this.auth.getClient();
654
+ const editResponse = await this.createEdit(authClient, this.packageName);
655
+ const editId = editResponse.data.id;
656
+ const session = {
657
+ auth: authClient,
658
+ packageName: this.packageName,
659
+ editId,
660
+ };
661
+ try {
662
+ const results = [];
663
+ for (const options of optionsList) {
664
+ console.error(`[GooglePlayClient] Preparing screenshots for ${options.language}...`);
665
+ const result = await this.uploadScreenshotsInSession(session, options);
666
+ results.push(result);
667
+ }
668
+ console.error(`[GooglePlayClient] Committing screenshots for ${optionsList.length} locale(s)...`);
669
+ await this.commitEdit(session);
670
+ console.error(`[GooglePlayClient] ✅ Screenshots committed for ${optionsList.length} locale(s)`);
671
+ return results;
672
+ }
673
+ catch (error) {
674
+ console.error(`[GooglePlayClient] Rolling back batch screenshot upload...`);
675
+ try {
676
+ await this.deleteEdit(session);
677
+ }
678
+ catch {
679
+ // Ignore deletion failure
680
+ }
681
+ throw error;
682
+ }
683
+ }
684
+ async uploadScreenshotsInSession(session, options) {
685
+ const { language, phoneScreenshots = [], sevenInchScreenshots = [], tenInchScreenshots = [], featureGraphic, imageUploadTimeoutMs, } = options;
632
686
  const result = {
633
687
  language,
634
688
  uploaded: {
@@ -638,115 +692,94 @@ export class GooglePlayClient {
638
692
  featureGraphic: false,
639
693
  },
640
694
  };
641
- try {
642
- // Delete existing screenshots before uploading
643
- if (phoneScreenshots.length > 0) {
644
- console.error(`[GooglePlayClient] Deleting existing phone screenshots for ${language}...`);
645
- try {
646
- await this.deleteAllImages(session, language, "phoneScreenshots");
647
- }
648
- catch (e) {
649
- // Ignore if no images exist
650
- if (e.code !== 404) {
651
- console.error(`[GooglePlayClient] Warning: Failed to delete phone screenshots: ${e.message}`);
652
- }
653
- }
695
+ if (phoneScreenshots.length > 0) {
696
+ console.error(`[GooglePlayClient] Deleting existing phone screenshots for ${language}...`);
697
+ try {
698
+ await this.deleteAllImages(session, language, "phoneScreenshots");
654
699
  }
655
- if (sevenInchScreenshots.length > 0) {
656
- console.error(`[GooglePlayClient] Deleting existing 7-inch screenshots for ${language}...`);
657
- try {
658
- await this.deleteAllImages(session, language, "sevenInchScreenshots");
659
- }
660
- catch (e) {
661
- if (e.code !== 404) {
662
- console.error(`[GooglePlayClient] Warning: Failed to delete 7-inch screenshots: ${e.message}`);
663
- }
700
+ catch (e) {
701
+ // Ignore if no images exist
702
+ if (e.code !== 404) {
703
+ console.error(`[GooglePlayClient] Warning: Failed to delete phone screenshots: ${e.message}`);
664
704
  }
665
705
  }
666
- if (tenInchScreenshots.length > 0) {
667
- console.error(`[GooglePlayClient] Deleting existing 10-inch screenshots for ${language}...`);
668
- try {
669
- await this.deleteAllImages(session, language, "tenInchScreenshots");
670
- }
671
- catch (e) {
672
- if (e.code !== 404) {
673
- console.error(`[GooglePlayClient] Warning: Failed to delete 10-inch screenshots: ${e.message}`);
674
- }
675
- }
706
+ }
707
+ if (sevenInchScreenshots.length > 0) {
708
+ console.error(`[GooglePlayClient] Deleting existing 7-inch screenshots for ${language}...`);
709
+ try {
710
+ await this.deleteAllImages(session, language, "sevenInchScreenshots");
676
711
  }
677
- if (featureGraphic) {
678
- console.error(`[GooglePlayClient] Deleting existing feature graphic for ${language}...`);
679
- try {
680
- await this.deleteAllImages(session, language, "featureGraphic");
681
- }
682
- catch (e) {
683
- if (e.code !== 404) {
684
- console.error(`[GooglePlayClient] Warning: Failed to delete feature graphic: ${e.message}`);
685
- }
712
+ catch (e) {
713
+ if (e.code !== 404) {
714
+ console.error(`[GooglePlayClient] Warning: Failed to delete 7-inch screenshots: ${e.message}`);
686
715
  }
687
716
  }
688
- // Upload phone screenshots
689
- for (let i = 0; i < phoneScreenshots.length; i++) {
690
- const imagePath = phoneScreenshots[i];
691
- if (!existsSync(imagePath)) {
692
- console.error(`[GooglePlayClient] Warning: Phone screenshot not found: ${imagePath}`);
693
- continue;
694
- }
695
- const imageBuffer = readFileSync(imagePath);
696
- const fileName = imagePath.split("/").pop() || `phone-${i + 1}.png`;
697
- await this.uploadImageWithOptionalTimeout(session, language, "phoneScreenshots", imageBuffer, imageUploadTimeoutMs);
698
- console.error(`[GooglePlayClient] ✅ Uploaded ${fileName}`);
699
- result.uploaded.phoneScreenshots++;
700
- }
701
- // Upload 7-inch tablet screenshots
702
- for (let i = 0; i < sevenInchScreenshots.length; i++) {
703
- const imagePath = sevenInchScreenshots[i];
704
- if (!existsSync(imagePath)) {
705
- console.error(`[GooglePlayClient] Warning: 7-inch screenshot not found: ${imagePath}`);
706
- continue;
707
- }
708
- const imageBuffer = readFileSync(imagePath);
709
- const fileName = imagePath.split("/").pop() || `tablet7-${i + 1}.png`;
710
- await this.uploadImageWithOptionalTimeout(session, language, "sevenInchScreenshots", imageBuffer, imageUploadTimeoutMs);
711
- console.error(`[GooglePlayClient] ✅ Uploaded ${fileName}`);
712
- result.uploaded.sevenInchScreenshots++;
713
- }
714
- // Upload 10-inch tablet screenshots
715
- for (let i = 0; i < tenInchScreenshots.length; i++) {
716
- const imagePath = tenInchScreenshots[i];
717
- if (!existsSync(imagePath)) {
718
- console.error(`[GooglePlayClient] Warning: 10-inch screenshot not found: ${imagePath}`);
719
- continue;
717
+ }
718
+ if (tenInchScreenshots.length > 0) {
719
+ console.error(`[GooglePlayClient] Deleting existing 10-inch screenshots for ${language}...`);
720
+ try {
721
+ await this.deleteAllImages(session, language, "tenInchScreenshots");
722
+ }
723
+ catch (e) {
724
+ if (e.code !== 404) {
725
+ console.error(`[GooglePlayClient] Warning: Failed to delete 10-inch screenshots: ${e.message}`);
720
726
  }
721
- const imageBuffer = readFileSync(imagePath);
722
- const fileName = imagePath.split("/").pop() || `tablet10-${i + 1}.png`;
723
- await this.uploadImageWithOptionalTimeout(session, language, "tenInchScreenshots", imageBuffer, imageUploadTimeoutMs);
724
- console.error(`[GooglePlayClient] ✅ Uploaded ${fileName}`);
725
- result.uploaded.tenInchScreenshots++;
726
- }
727
- // Upload feature graphic
728
- if (featureGraphic && existsSync(featureGraphic)) {
729
- const imageBuffer = readFileSync(featureGraphic);
730
- await this.uploadImageWithOptionalTimeout(session, language, "featureGraphic", imageBuffer, imageUploadTimeoutMs);
731
- console.error(`[GooglePlayClient] ✅ Uploaded feature-graphic.png`);
732
- result.uploaded.featureGraphic = true;
733
727
  }
734
- // Commit all changes
735
- console.error(`[GooglePlayClient] Committing screenshots for ${language}...`);
736
- await this.commitEdit(session);
737
- console.error(`[GooglePlayClient] ✅ Screenshots committed for ${language}`);
738
- return result;
739
728
  }
740
- catch (error) {
741
- console.error(`[GooglePlayClient] Rolling back screenshot upload for ${language}...`);
729
+ if (featureGraphic) {
730
+ console.error(`[GooglePlayClient] Deleting existing feature graphic for ${language}...`);
742
731
  try {
743
- await this.deleteEdit(session);
732
+ await this.deleteAllImages(session, language, "featureGraphic");
744
733
  }
745
- catch {
746
- // Ignore deletion failure
734
+ catch (e) {
735
+ if (e.code !== 404) {
736
+ console.error(`[GooglePlayClient] Warning: Failed to delete feature graphic: ${e.message}`);
737
+ }
747
738
  }
748
- throw error;
749
739
  }
740
+ for (let i = 0; i < phoneScreenshots.length; i++) {
741
+ const imagePath = phoneScreenshots[i];
742
+ if (!existsSync(imagePath)) {
743
+ console.error(`[GooglePlayClient] Warning: Phone screenshot not found: ${imagePath}`);
744
+ continue;
745
+ }
746
+ const imageBuffer = readFileSync(imagePath);
747
+ const fileName = imagePath.split("/").pop() || `phone-${i + 1}.png`;
748
+ await this.uploadImageWithOptionalTimeout(session, language, "phoneScreenshots", imageBuffer, imageUploadTimeoutMs);
749
+ console.error(`[GooglePlayClient] ✅ Uploaded ${fileName}`);
750
+ result.uploaded.phoneScreenshots++;
751
+ }
752
+ for (let i = 0; i < sevenInchScreenshots.length; i++) {
753
+ const imagePath = sevenInchScreenshots[i];
754
+ if (!existsSync(imagePath)) {
755
+ console.error(`[GooglePlayClient] Warning: 7-inch screenshot not found: ${imagePath}`);
756
+ continue;
757
+ }
758
+ const imageBuffer = readFileSync(imagePath);
759
+ const fileName = imagePath.split("/").pop() || `tablet7-${i + 1}.png`;
760
+ await this.uploadImageWithOptionalTimeout(session, language, "sevenInchScreenshots", imageBuffer, imageUploadTimeoutMs);
761
+ console.error(`[GooglePlayClient] ✅ Uploaded ${fileName}`);
762
+ result.uploaded.sevenInchScreenshots++;
763
+ }
764
+ for (let i = 0; i < tenInchScreenshots.length; i++) {
765
+ const imagePath = tenInchScreenshots[i];
766
+ if (!existsSync(imagePath)) {
767
+ console.error(`[GooglePlayClient] Warning: 10-inch screenshot not found: ${imagePath}`);
768
+ continue;
769
+ }
770
+ const imageBuffer = readFileSync(imagePath);
771
+ const fileName = imagePath.split("/").pop() || `tablet10-${i + 1}.png`;
772
+ await this.uploadImageWithOptionalTimeout(session, language, "tenInchScreenshots", imageBuffer, imageUploadTimeoutMs);
773
+ console.error(`[GooglePlayClient] ✅ Uploaded ${fileName}`);
774
+ result.uploaded.tenInchScreenshots++;
775
+ }
776
+ if (featureGraphic && existsSync(featureGraphic)) {
777
+ const imageBuffer = readFileSync(featureGraphic);
778
+ await this.uploadImageWithOptionalTimeout(session, language, "featureGraphic", imageBuffer, imageUploadTimeoutMs);
779
+ console.error(`[GooglePlayClient] ✅ Uploaded feature-graphic.png`);
780
+ result.uploaded.featureGraphic = true;
781
+ }
782
+ return result;
750
783
  }
751
784
  async uploadImageWithOptionalTimeout(session, language, imageType, imageBuffer, timeoutMs) {
752
785
  await this.androidPublisher.edits.images.upload({
@@ -5,11 +5,14 @@ import { AppResolutionService } from "../../core/services/app-resolution-service
5
5
  import { AppStoreService } from "../../core/services/app-store-service.js";
6
6
  import { GooglePlayService } from "../../core/services/google-play-service.js";
7
7
  import { formatPushResult } from "../../core/helpers/formatters.js";
8
+ const DEFAULT_IMAGE_UPLOAD_TIMEOUT_MS = 10 * 60 * 1000;
8
9
  const appResolutionService = new AppResolutionService();
9
10
  const appStoreService = new AppStoreService();
10
11
  const googlePlayService = new GooglePlayService();
11
12
  export async function handleAsoPush(options) {
12
- const { store = "both", uploadImages = false, locales, imageUploadTimeoutMs, dryRun = false, } = options;
13
+ const { store = "both", uploadImages = false, locales, dryRun = false, } = options;
14
+ const imageUploadTimeoutMs = options.imageUploadTimeoutMs ??
15
+ (uploadImages ? DEFAULT_IMAGE_UPLOAD_TIMEOUT_MS : undefined);
13
16
  const resolved = appResolutionService.resolve({
14
17
  slug: options.app,
15
18
  packageName: options.packageName,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pabal-store-api-mcp",
3
- "version": "1.3.11",
3
+ "version": "1.3.14",
4
4
  "description": "MCP server for App Store / Play Store ASO workflows",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",