pabal-store-api-mcp 1.3.13 → 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.
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
|
|
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
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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,6 +783,9 @@ 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) {
|
|
784
791
|
if (screenshot.type !== "appScreenshots") {
|
|
@@ -790,6 +797,33 @@ export class AppStoreClient {
|
|
|
790
797
|
}
|
|
791
798
|
return deletedCount;
|
|
792
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
|
+
}
|
|
793
827
|
sanitizePathSegment(value) {
|
|
794
828
|
return value.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
795
829
|
}
|
|
@@ -808,9 +842,19 @@ export class AppStoreClient {
|
|
|
808
842
|
catch (error) {
|
|
809
843
|
const code = error.code;
|
|
810
844
|
if (code === "EEXIST") {
|
|
811
|
-
|
|
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;
|
|
812
857
|
}
|
|
813
|
-
throw error;
|
|
814
858
|
}
|
|
815
859
|
closeSync(descriptor);
|
|
816
860
|
return () => {
|
|
@@ -856,10 +900,27 @@ export class AppStoreClient {
|
|
|
856
900
|
}
|
|
857
901
|
}
|
|
858
902
|
/**
|
|
859
|
-
* 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
|
+
*
|
|
860
920
|
* 1. Find or create screenshot sets for each display type
|
|
861
|
-
* 2. Delete existing screenshots
|
|
921
|
+
* 2. Delete only enough existing screenshots to fit the incoming batch
|
|
862
922
|
* 3. Upload new screenshots in order
|
|
923
|
+
* 4. Delete remaining old screenshots after the batch succeeds
|
|
863
924
|
*/
|
|
864
925
|
async uploadScreenshotsForLocale(options) {
|
|
865
926
|
const { locale, screenshots, imageUploadTimeoutMs } = options;
|
|
@@ -906,13 +967,10 @@ export class AppStoreClient {
|
|
|
906
967
|
console.error(`[AppStore] Processing ${displayType} (${screenshotList.length} screenshots)...`);
|
|
907
968
|
// Find or create screenshot set
|
|
908
969
|
const screenshotSetId = await this.findOrCreateScreenshotSet(localizationId, displayType);
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
if (deletedCount > 0) {
|
|
912
|
-
console.error(`[AppStore] Deleted ${deletedCount} existing screenshots`);
|
|
913
|
-
result.deleted += deletedCount;
|
|
914
|
-
}
|
|
970
|
+
const { deletedBeforeUpload, screenshotsToDeleteAfterUpload } = await this.prepareScreenshotSetForUpload(screenshotSetId, screenshotList.length);
|
|
971
|
+
result.deleted += deletedBeforeUpload;
|
|
915
972
|
// Upload new screenshots in order
|
|
973
|
+
let uploadedForDisplayType = 0;
|
|
916
974
|
for (const screenshot of screenshotList) {
|
|
917
975
|
try {
|
|
918
976
|
const fileBuffer = readFileSync(screenshot.path);
|
|
@@ -929,12 +987,20 @@ export class AppStoreClient {
|
|
|
929
987
|
await this.commitAppScreenshot(screenshotData.id);
|
|
930
988
|
console.error(`[AppStore] ✅ ${screenshot.filename}`);
|
|
931
989
|
result.uploaded++;
|
|
990
|
+
uploadedForDisplayType++;
|
|
932
991
|
}
|
|
933
992
|
catch (error) {
|
|
934
993
|
result.failed++;
|
|
935
994
|
throw new Error(`[${displayType}] ${screenshot.filename}: ${error instanceof Error ? error.message : String(error)}`);
|
|
936
995
|
}
|
|
937
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
|
+
}
|
|
938
1004
|
}
|
|
939
1005
|
finally {
|
|
940
1006
|
releaseLock();
|
|
@@ -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,
|
|
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,
|