@yoonion/mimi-seed-mcp 0.3.3 → 0.3.5
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/appstore/tools.d.ts +28 -1
- package/dist/appstore/tools.js +133 -8
- package/dist/index.js +65 -3
- package/dist/playstore/tools.d.ts +22 -0
- package/dist/playstore/tools.js +73 -0
- package/package.json +1 -1
package/dist/appstore/tools.d.ts
CHANGED
|
@@ -20,6 +20,25 @@ export declare function updateVersionWhatsNew(versionId: string, locale: string,
|
|
|
20
20
|
export declare function listBuilds(appId: string): Promise<any>;
|
|
21
21
|
export declare function listBetaGroups(appId: string): Promise<any>;
|
|
22
22
|
export declare function getAppInfo(appId: string): Promise<any>;
|
|
23
|
+
export interface AppInfoLocalizationFields {
|
|
24
|
+
name?: string;
|
|
25
|
+
subtitle?: string;
|
|
26
|
+
privacyPolicyUrl?: string;
|
|
27
|
+
privacyPolicyText?: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function listAppInfoLocalizations(appId: string, locale?: string): Promise<{
|
|
30
|
+
appInfoId: string;
|
|
31
|
+
appInfoState: string;
|
|
32
|
+
localizations: {
|
|
33
|
+
id: any;
|
|
34
|
+
locale: any;
|
|
35
|
+
name: any;
|
|
36
|
+
subtitle: any;
|
|
37
|
+
privacyPolicyUrl: any;
|
|
38
|
+
privacyPolicyText: any;
|
|
39
|
+
}[];
|
|
40
|
+
}>;
|
|
41
|
+
export declare function updateAppInfoLocalization(localizationId: string, fields: AppInfoLocalizationFields): Promise<any>;
|
|
23
42
|
export interface ListCustomerReviewsOptions {
|
|
24
43
|
limit?: number;
|
|
25
44
|
territory?: string;
|
|
@@ -27,4 +46,12 @@ export interface ListCustomerReviewsOptions {
|
|
|
27
46
|
}
|
|
28
47
|
export declare function listCustomerReviews(appId: string, opts?: ListCustomerReviewsOptions): Promise<any>;
|
|
29
48
|
export declare function createReviewResponse(reviewId: string, responseBody: string): Promise<any>;
|
|
30
|
-
export declare function submitVersionForReview(versionId: string): Promise<
|
|
49
|
+
export declare function submitVersionForReview(versionId: string): Promise<{
|
|
50
|
+
submissionId: string;
|
|
51
|
+
appId: string;
|
|
52
|
+
platform: string;
|
|
53
|
+
versionId: string;
|
|
54
|
+
reusedSubmission: boolean;
|
|
55
|
+
itemAttached: boolean;
|
|
56
|
+
state: any;
|
|
57
|
+
}>;
|
package/dist/appstore/tools.js
CHANGED
|
@@ -174,16 +174,64 @@ export async function listBetaGroups(appId) {
|
|
|
174
174
|
}));
|
|
175
175
|
}
|
|
176
176
|
// ─── 앱 정보 (카테고리 등) ───
|
|
177
|
+
// AppInfoState — READY_FOR_DISTRIBUTION이 라이브 버전, 그 외(PREPARE_FOR_SUBMISSION /
|
|
178
|
+
// DEVELOPER_REJECTED 등)가 편집 가능한 appInfo.
|
|
179
|
+
const APP_INFO_LIVE_STATE = 'READY_FOR_DISTRIBUTION';
|
|
177
180
|
export async function getAppInfo(appId) {
|
|
178
181
|
const data = await apiGet(`/apps/${appId}/appInfos`, {
|
|
179
|
-
'fields[appInfos]': '
|
|
182
|
+
'fields[appInfos]': 'state,appStoreAgeRating,brazilAgeRating',
|
|
180
183
|
});
|
|
181
184
|
return (data.data ?? []).map((i) => ({
|
|
182
185
|
id: i.id,
|
|
183
|
-
state
|
|
186
|
+
// 새 필드명 state, 옛 필드명 appStoreState 모두 케어
|
|
187
|
+
state: i.attributes?.state ?? i.attributes?.appStoreState,
|
|
184
188
|
ageRating: i.attributes?.appStoreAgeRating,
|
|
185
189
|
}));
|
|
186
190
|
}
|
|
191
|
+
async function findEditableAppInfoId(appId) {
|
|
192
|
+
const data = await apiGet(`/apps/${appId}/appInfos`, {
|
|
193
|
+
'fields[appInfos]': 'state',
|
|
194
|
+
'limit': '10',
|
|
195
|
+
});
|
|
196
|
+
const infos = (data?.data ?? []);
|
|
197
|
+
if (infos.length === 0) {
|
|
198
|
+
throw new Error(`앱 ${appId}에 appInfos가 없어. 앱 ID 확인 필요.`);
|
|
199
|
+
}
|
|
200
|
+
const stateOf = (i) => i.attributes?.state ?? i.attributes?.appStoreState ?? '';
|
|
201
|
+
const editable = infos.find((i) => stateOf(i) !== APP_INFO_LIVE_STATE);
|
|
202
|
+
const target = editable ?? infos[0];
|
|
203
|
+
return { appInfoId: target.id, state: stateOf(target) };
|
|
204
|
+
}
|
|
205
|
+
export async function listAppInfoLocalizations(appId, locale) {
|
|
206
|
+
const { appInfoId, state } = await findEditableAppInfoId(appId);
|
|
207
|
+
const data = await apiGet(`/appInfos/${appInfoId}/appInfoLocalizations`, {
|
|
208
|
+
'fields[appInfoLocalizations]': 'locale,name,subtitle,privacyPolicyUrl,privacyPolicyText',
|
|
209
|
+
'limit': '200',
|
|
210
|
+
});
|
|
211
|
+
const all = (data?.data ?? []).map((l) => ({
|
|
212
|
+
id: l.id,
|
|
213
|
+
locale: l.attributes?.locale,
|
|
214
|
+
name: l.attributes?.name,
|
|
215
|
+
subtitle: l.attributes?.subtitle,
|
|
216
|
+
privacyPolicyUrl: l.attributes?.privacyPolicyUrl,
|
|
217
|
+
privacyPolicyText: l.attributes?.privacyPolicyText,
|
|
218
|
+
}));
|
|
219
|
+
const filtered = locale ? all.filter((l) => l.locale === locale) : all;
|
|
220
|
+
return { appInfoId, appInfoState: state, localizations: filtered };
|
|
221
|
+
}
|
|
222
|
+
export async function updateAppInfoLocalization(localizationId, fields) {
|
|
223
|
+
const attributes = Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== undefined));
|
|
224
|
+
if (Object.keys(attributes).length === 0) {
|
|
225
|
+
throw new Error('수정할 필드가 없어 (name / subtitle / privacyPolicyUrl / privacyPolicyText 중 하나 이상).');
|
|
226
|
+
}
|
|
227
|
+
return apiPatch(`/appInfoLocalizations/${localizationId}`, {
|
|
228
|
+
data: {
|
|
229
|
+
type: 'appInfoLocalizations',
|
|
230
|
+
id: localizationId,
|
|
231
|
+
attributes,
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
}
|
|
187
235
|
export async function listCustomerReviews(appId, opts = {}) {
|
|
188
236
|
const params = {
|
|
189
237
|
'sort': '-createdDate',
|
|
@@ -237,15 +285,92 @@ export async function createReviewResponse(reviewId, responseBody) {
|
|
|
237
285
|
});
|
|
238
286
|
}
|
|
239
287
|
// ─── 심사 제출 (Submit for Review) ───
|
|
288
|
+
// 2024년 1월부터 옛 /appStoreVersionSubmissions가 deprecated 됐고,
|
|
289
|
+
// 새 모델은 /reviewSubmissions + /reviewSubmissionItems 2단계 + PATCH submitted=true.
|
|
290
|
+
// 참고: https://developer.apple.com/documentation/appstoreconnectapi/submit-an-app-for-review
|
|
291
|
+
async function getVersionAppAndPlatform(versionId) {
|
|
292
|
+
const data = await apiGet(`/appStoreVersions/${versionId}`, {
|
|
293
|
+
'fields[appStoreVersions]': 'platform,app',
|
|
294
|
+
'include': 'app',
|
|
295
|
+
});
|
|
296
|
+
const platform = data?.data?.attributes?.platform;
|
|
297
|
+
const appId = data?.data?.relationships?.app?.data?.id;
|
|
298
|
+
if (!platform || !appId) {
|
|
299
|
+
throw new Error(`appStoreVersion ${versionId}에서 app 또는 platform을 찾지 못했어. 버전 ID 확인 필요.`);
|
|
300
|
+
}
|
|
301
|
+
return { appId, platform };
|
|
302
|
+
}
|
|
303
|
+
async function findOpenReviewSubmission(appId, platform) {
|
|
304
|
+
const data = await apiGet('/reviewSubmissions', {
|
|
305
|
+
'filter[app]': appId,
|
|
306
|
+
'filter[platform]': platform,
|
|
307
|
+
'filter[state]': 'READY_FOR_REVIEW,COMPLETING,UNRESOLVED_ISSUES',
|
|
308
|
+
'limit': '1',
|
|
309
|
+
});
|
|
310
|
+
// CREATED 상태(아직 제출 안 된)도 별도 조회해서 우선 재사용
|
|
311
|
+
const created = await apiGet('/reviewSubmissions', {
|
|
312
|
+
'filter[app]': appId,
|
|
313
|
+
'filter[platform]': platform,
|
|
314
|
+
'filter[state]': 'CREATED',
|
|
315
|
+
'limit': '1',
|
|
316
|
+
});
|
|
317
|
+
return created?.data?.[0]?.id ?? data?.data?.[0]?.id ?? null;
|
|
318
|
+
}
|
|
319
|
+
async function isVersionAttached(submissionId, versionId) {
|
|
320
|
+
const data = await apiGet(`/reviewSubmissions/${submissionId}/items`, {
|
|
321
|
+
'limit': '50',
|
|
322
|
+
});
|
|
323
|
+
const items = (data?.data ?? []);
|
|
324
|
+
return items.some((it) => it?.relationships?.appStoreVersion?.data?.id === versionId);
|
|
325
|
+
}
|
|
240
326
|
export async function submitVersionForReview(versionId) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
327
|
+
const { appId, platform } = await getVersionAppAndPlatform(versionId);
|
|
328
|
+
// 1. CREATED 상태의 reviewSubmission이 있으면 재사용, 없으면 새로 생성
|
|
329
|
+
let submissionId = await findOpenReviewSubmission(appId, platform);
|
|
330
|
+
const reusedSubmission = Boolean(submissionId);
|
|
331
|
+
if (!submissionId) {
|
|
332
|
+
const created = await apiPost('/reviewSubmissions', {
|
|
333
|
+
data: {
|
|
334
|
+
type: 'reviewSubmissions',
|
|
335
|
+
attributes: { platform },
|
|
336
|
+
relationships: {
|
|
337
|
+
app: { data: { type: 'apps', id: appId } },
|
|
247
338
|
},
|
|
248
339
|
},
|
|
340
|
+
});
|
|
341
|
+
submissionId = created?.data?.id;
|
|
342
|
+
if (!submissionId) {
|
|
343
|
+
throw new Error(`reviewSubmission 생성 응답에 id가 없어: ${JSON.stringify(created)}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// 2. 버전을 reviewSubmissionItems로 attach (이미 붙어있으면 skip)
|
|
347
|
+
const alreadyAttached = reusedSubmission ? await isVersionAttached(submissionId, versionId) : false;
|
|
348
|
+
if (!alreadyAttached) {
|
|
349
|
+
await apiPost('/reviewSubmissionItems', {
|
|
350
|
+
data: {
|
|
351
|
+
type: 'reviewSubmissionItems',
|
|
352
|
+
relationships: {
|
|
353
|
+
reviewSubmission: { data: { type: 'reviewSubmissions', id: submissionId } },
|
|
354
|
+
appStoreVersion: { data: { type: 'appStoreVersions', id: versionId } },
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
// 3. PATCH submitted=true → state: CREATED → WAITING_FOR_REVIEW
|
|
360
|
+
const submitted = await apiPatch(`/reviewSubmissions/${submissionId}`, {
|
|
361
|
+
data: {
|
|
362
|
+
type: 'reviewSubmissions',
|
|
363
|
+
id: submissionId,
|
|
364
|
+
attributes: { submitted: true },
|
|
249
365
|
},
|
|
250
366
|
});
|
|
367
|
+
return {
|
|
368
|
+
submissionId,
|
|
369
|
+
appId,
|
|
370
|
+
platform,
|
|
371
|
+
versionId,
|
|
372
|
+
reusedSubmission,
|
|
373
|
+
itemAttached: !alreadyAttached,
|
|
374
|
+
state: submitted?.data?.attributes?.state ?? 'WAITING_FOR_REVIEW',
|
|
375
|
+
};
|
|
251
376
|
}
|
package/dist/index.js
CHANGED
|
@@ -631,10 +631,37 @@ server.tool('appstore_list_beta_groups', 'TestFlight 베타 그룹 목록', { ap
|
|
|
631
631
|
const groups = await appstore.listBetaGroups(appId);
|
|
632
632
|
return { content: [{ type: 'text', text: JSON.stringify(groups, null, 2) }] };
|
|
633
633
|
});
|
|
634
|
-
server.tool('appstore_get_app_info', 'App Store 앱 정보 (카테고리, 연령
|
|
634
|
+
server.tool('appstore_get_app_info', 'App Store 앱 정보 (카테고리, 연령 등급, state). state=READY_FOR_DISTRIBUTION이 라이브, 그 외가 편집 가능 appInfo.', { appId: z.string().describe('앱 ID') }, async ({ appId }) => {
|
|
635
635
|
const info = await appstore.getAppInfo(appId);
|
|
636
636
|
return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] };
|
|
637
637
|
});
|
|
638
|
+
server.tool('appstore_list_app_info_localizations', [
|
|
639
|
+
'편집 가능한 appInfo의 로컬라이제이션 목록 조회 — 앱 이름(name), 부제(subtitle), 개인정보 URL/텍스트.',
|
|
640
|
+
'appInfo.relationships.appInfoLocalizations가 빈 배열로 오는 케이스를 우회하려고 /appInfos/{id}/appInfoLocalizations 직접 호출.',
|
|
641
|
+
'locale을 주면 해당 언어만 반환 (예: "ko", "en-US").',
|
|
642
|
+
'※ versionLocalization(설명/키워드/whatsNew)과 다름 — 그건 appstore_get_metadata 사용.',
|
|
643
|
+
].join(' '), {
|
|
644
|
+
appId: z.string().describe('앱 ID (appstore_list_apps 결과)'),
|
|
645
|
+
locale: z.string().optional().describe('언어 필터 (예: "ko", "en-US"). 생략 시 전체 반환.'),
|
|
646
|
+
}, async ({ appId, locale }) => {
|
|
647
|
+
const result = await appstore.listAppInfoLocalizations(appId, locale);
|
|
648
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
649
|
+
});
|
|
650
|
+
server.tool('appstore_update_app_info_localization', [
|
|
651
|
+
'appInfo 로컬라이제이션(앱 이름/부제/개인정보 URL/텍스트) 수정 — PATCH /appInfoLocalizations/{id}.',
|
|
652
|
+
'localizationId는 appstore_list_app_info_localizations 결과의 id.',
|
|
653
|
+
'편집 가능 상태(PREPARE_FOR_SUBMISSION / DEVELOPER_REJECTED 등)에서만 반영됨.',
|
|
654
|
+
'제한: name 30자, subtitle 30자.',
|
|
655
|
+
].join(' '), {
|
|
656
|
+
localizationId: z.string().describe('appInfoLocalization ID'),
|
|
657
|
+
name: z.string().optional().describe('앱 이름 (30자 이내)'),
|
|
658
|
+
subtitle: z.string().optional().describe('부제 (30자 이내)'),
|
|
659
|
+
privacyPolicyUrl: z.string().url().optional().describe('개인정보 처리방침 URL'),
|
|
660
|
+
privacyPolicyText: z.string().optional().describe('개인정보 처리방침 텍스트'),
|
|
661
|
+
}, async ({ localizationId, ...fields }) => {
|
|
662
|
+
const result = await appstore.updateAppInfoLocalization(localizationId, fields);
|
|
663
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
664
|
+
});
|
|
638
665
|
// --- App Store 고객 리뷰 ---
|
|
639
666
|
server.tool('appstore_list_reviews', [
|
|
640
667
|
'App Store 받은 고객 리뷰 조회 (최신순).',
|
|
@@ -663,7 +690,9 @@ server.tool('appstore_reply_review', [
|
|
|
663
690
|
});
|
|
664
691
|
// --- App Store 심사 제출 (Submit for Review) ---
|
|
665
692
|
server.tool('appstore_submit_for_review', [
|
|
666
|
-
'App Store 버전을 심사에 제출 —
|
|
693
|
+
'App Store 버전을 심사에 제출 — 새 reviewSubmissions API 사용 (옛 /appStoreVersionSubmissions는 2024-01 deprecated).',
|
|
694
|
+
'내부 흐름: POST /reviewSubmissions(또는 CREATED 상태 재사용) → POST /reviewSubmissionItems(version attach) → PATCH submitted=true.',
|
|
695
|
+
'appId와 platform은 versionId에서 자동 조회 — 별도 입력 불필요.',
|
|
667
696
|
'⚠️ 비가역 작업: 제출 후엔 Apple 심사가 시작되며, 메타데이터/스크린샷/빌드를 더 못 바꿈 (REJECTED/METADATA_REJECTED 시 다시 편집 가능).',
|
|
668
697
|
'사전 조건: 버전이 PREPARE_FOR_SUBMISSION 또는 DEVELOPER_REJECTED 상태, 빌드 attached, 모든 필수 메타데이터 채워짐.',
|
|
669
698
|
'appstore_check_submission_risks로 사전 점검 권장.',
|
|
@@ -671,7 +700,7 @@ server.tool('appstore_submit_for_review', [
|
|
|
671
700
|
versionId: z.string().describe('App Store 버전 ID (appstore_list_versions 결과)'),
|
|
672
701
|
}, async ({ versionId }) => {
|
|
673
702
|
const result = await appstore.submitVersionForReview(versionId);
|
|
674
|
-
return { content: [{ type: 'text', text: `✅ 버전 ${versionId} 심사 제출
|
|
703
|
+
return { content: [{ type: 'text', text: `✅ 버전 ${versionId} 심사 제출 완료 (state: ${result.state}). App Store Connect에서 진행 상태 확인 가능.\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
675
704
|
});
|
|
676
705
|
// --- Play Store 심사 제출 / release status 변경 ---
|
|
677
706
|
server.tool('playstore_submit_release', [
|
|
@@ -689,6 +718,39 @@ server.tool('playstore_submit_release', [
|
|
|
689
718
|
const result = await playstore.submitRelease(auth, packageName, track, versionCode, status);
|
|
690
719
|
return { content: [{ type: 'text', text: `✅ ${packageName} ${track} v${versionCode}: ${result.previousStatus} → ${result.newStatus}\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
691
720
|
});
|
|
721
|
+
// --- Play Store 트랙 간 promote (internal → production 등) ---
|
|
722
|
+
server.tool('playstore_promote_release', [
|
|
723
|
+
'Google Play 트랙 간 release promote — fromTrack(예: internal)의 versionCode를 toTrack(예: production)에 새 release로 추가.',
|
|
724
|
+
'단일 edit session에서 source 조회 → target 업데이트 → commit 까지 한 번에 처리.',
|
|
725
|
+
'source의 releaseNotes를 자동 복사 (copyReleaseNotes=false 또는 releaseNotes로 덮어쓰기 가능).',
|
|
726
|
+
'status="completed" + production 으로 전체 출시, status="inProgress" + userFraction 으로 단계 출시(예: 0.1 = 10%).',
|
|
727
|
+
'target에 같은 versionCode가 이미 있으면 해당 항목을 교체. status="completed"면 target 활성 release를 통째로 새 것으로 대체.',
|
|
728
|
+
'⚠️ status="completed"는 비가역에 가까움. playstore_check_submission_risks로 사전 점검 권장.',
|
|
729
|
+
].join(' '), {
|
|
730
|
+
packageName: z.string().describe('패키지명 (예: gg.pryzm.coffee)'),
|
|
731
|
+
fromTrack: z.enum(['production', 'beta', 'alpha', 'internal']).describe('출처 트랙 (예: internal)'),
|
|
732
|
+
toTrack: z.enum(['production', 'beta', 'alpha', 'internal']).describe('대상 트랙 (예: production)'),
|
|
733
|
+
versionCode: z.string().describe('promote할 versionCode (문자열)'),
|
|
734
|
+
status: z.enum(['completed', 'draft', 'inProgress', 'halted']).optional().describe('대상 트랙에서의 status (기본 completed)'),
|
|
735
|
+
userFraction: z.number().min(0).max(1).optional().describe('status="inProgress"일 때 단계 출시 비율 (0~1, 예: 0.1)'),
|
|
736
|
+
releaseName: z.string().optional().describe('release 이름 (미지정 시 source 이름 그대로)'),
|
|
737
|
+
copyReleaseNotes: z.boolean().optional().describe('source releaseNotes 복사 여부 (기본 true)'),
|
|
738
|
+
releaseNotes: z.array(z.object({
|
|
739
|
+
language: z.string().describe('언어 코드 (예: ko-KR, en-US)'),
|
|
740
|
+
text: z.string().describe('릴리스 노트 본문 (500자 이내)'),
|
|
741
|
+
})).optional().describe('대상 트랙용 릴리스 노트 (미지정 + copyReleaseNotes=true면 source 그대로)'),
|
|
742
|
+
}, async ({ packageName, fromTrack, toTrack, versionCode, status, userFraction, releaseName, copyReleaseNotes, releaseNotes }) => {
|
|
743
|
+
const auth = requireAuth();
|
|
744
|
+
const result = await playstore.promoteRelease(auth, packageName, fromTrack, toTrack, versionCode, {
|
|
745
|
+
status,
|
|
746
|
+
userFraction,
|
|
747
|
+
releaseName,
|
|
748
|
+
copyReleaseNotes,
|
|
749
|
+
releaseNotes,
|
|
750
|
+
});
|
|
751
|
+
const summary = `✅ ${packageName} ${fromTrack} → ${toTrack} v${versionCode} (status: ${result.newStatus}${result.userFraction != null ? `, userFraction: ${result.userFraction}` : ''})`;
|
|
752
|
+
return { content: [{ type: 'text', text: `${summary}\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
753
|
+
});
|
|
692
754
|
// --- 인증 상태 ---
|
|
693
755
|
server.tool('mimi_seed_auth_start', [
|
|
694
756
|
'Google OAuth 로그인 링크를 발급하고 백그라운드 콜백 서버를 시작.',
|
|
@@ -62,6 +62,28 @@ export declare function submitRelease(auth: OAuth2Client, packageName: string, t
|
|
|
62
62
|
committed: boolean;
|
|
63
63
|
result: import("googleapis").androidpublisher_v3.Schema$Track;
|
|
64
64
|
}>;
|
|
65
|
+
export interface PromoteReleaseOptions {
|
|
66
|
+
status?: 'completed' | 'draft' | 'inProgress' | 'halted';
|
|
67
|
+
userFraction?: number;
|
|
68
|
+
releaseName?: string;
|
|
69
|
+
releaseNotes?: Array<{
|
|
70
|
+
language: string;
|
|
71
|
+
text: string;
|
|
72
|
+
}>;
|
|
73
|
+
copyReleaseNotes?: boolean;
|
|
74
|
+
}
|
|
75
|
+
export declare function promoteRelease(auth: OAuth2Client, packageName: string, fromTrack: string, toTrack: string, versionCode: string, options?: PromoteReleaseOptions): Promise<{
|
|
76
|
+
packageName: string;
|
|
77
|
+
fromTrack: string;
|
|
78
|
+
toTrack: string;
|
|
79
|
+
versionCode: string;
|
|
80
|
+
newStatus: "completed" | "draft" | "inProgress" | "halted";
|
|
81
|
+
userFraction: number | undefined;
|
|
82
|
+
releaseName: string | null | undefined;
|
|
83
|
+
releaseNotesLanguages: (string | null | undefined)[];
|
|
84
|
+
committed: boolean;
|
|
85
|
+
result: import("googleapis").androidpublisher_v3.Schema$Track;
|
|
86
|
+
}>;
|
|
65
87
|
export declare function listReviews(auth: OAuth2Client, packageName: string): Promise<{
|
|
66
88
|
reviewId: string | null | undefined;
|
|
67
89
|
authorName: string | null | undefined;
|
package/dist/playstore/tools.js
CHANGED
|
@@ -262,6 +262,79 @@ export async function submitRelease(auth, packageName, track, versionCode, statu
|
|
|
262
262
|
};
|
|
263
263
|
}, true);
|
|
264
264
|
}
|
|
265
|
+
export async function promoteRelease(auth, packageName, fromTrack, toTrack, versionCode, options = {}) {
|
|
266
|
+
const { status = 'completed', userFraction, releaseName, releaseNotes, copyReleaseNotes = true, } = options;
|
|
267
|
+
if (fromTrack === toTrack) {
|
|
268
|
+
throw new Error('fromTrack과 toTrack이 같아. 다른 트랙으로 promote 해야 의미 있어.');
|
|
269
|
+
}
|
|
270
|
+
if (status === 'inProgress' && (userFraction == null || userFraction <= 0 || userFraction >= 1)) {
|
|
271
|
+
throw new Error('status="inProgress"일 때 userFraction은 0과 1 사이 필수 (예: 0.1 → 10%).');
|
|
272
|
+
}
|
|
273
|
+
return withEdit(auth, packageName, async (editId) => {
|
|
274
|
+
// 1. source 트랙에서 versionCode 매칭 release 찾기
|
|
275
|
+
const fromData = await publisher().edits.tracks.get({
|
|
276
|
+
auth, packageName, editId, track: fromTrack,
|
|
277
|
+
});
|
|
278
|
+
const fromReleases = fromData.data.releases ?? [];
|
|
279
|
+
const sourceRelease = fromReleases.find((r) => (r.versionCodes ?? []).some((v) => String(v) === String(versionCode)));
|
|
280
|
+
if (!sourceRelease) {
|
|
281
|
+
const available = fromReleases.map((r) => ({
|
|
282
|
+
name: r.name,
|
|
283
|
+
versionCodes: r.versionCodes,
|
|
284
|
+
status: r.status,
|
|
285
|
+
}));
|
|
286
|
+
throw new Error(`versionCode "${versionCode}"를 ${fromTrack} 트랙에서 찾을 수 없어. 가능한 릴리스: ${JSON.stringify(available)}`);
|
|
287
|
+
}
|
|
288
|
+
// 2. 새 release 객체 구성
|
|
289
|
+
const newRelease = {
|
|
290
|
+
name: releaseName ?? sourceRelease.name ?? versionCode,
|
|
291
|
+
versionCodes: [String(versionCode)],
|
|
292
|
+
status,
|
|
293
|
+
releaseNotes: releaseNotes ??
|
|
294
|
+
(copyReleaseNotes ? sourceRelease.releaseNotes ?? [] : []),
|
|
295
|
+
};
|
|
296
|
+
if (status === 'inProgress' && userFraction != null) {
|
|
297
|
+
newRelease.userFraction = userFraction;
|
|
298
|
+
}
|
|
299
|
+
// 3. target 트랙 현재 상태 조회 후 merge
|
|
300
|
+
// - 같은 versionCode가 이미 target에 있으면 그 항목을 새 release로 교체
|
|
301
|
+
// - status='completed'면 활성 release가 1개여야 하므로 [newRelease]로 통째 교체
|
|
302
|
+
// - 그 외(draft/inProgress)는 기존 release에 append
|
|
303
|
+
const toData = await publisher().edits.tracks.get({
|
|
304
|
+
auth, packageName, editId, track: toTrack,
|
|
305
|
+
});
|
|
306
|
+
const toReleases = toData.data.releases ?? [];
|
|
307
|
+
const existingIdx = toReleases.findIndex((r) => (r.versionCodes ?? []).some((v) => String(v) === String(versionCode)));
|
|
308
|
+
let mergedReleases;
|
|
309
|
+
if (existingIdx >= 0) {
|
|
310
|
+
mergedReleases = [...toReleases];
|
|
311
|
+
mergedReleases[existingIdx] = newRelease;
|
|
312
|
+
}
|
|
313
|
+
else if (status === 'completed') {
|
|
314
|
+
mergedReleases = [newRelease];
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
mergedReleases = [...toReleases, newRelease];
|
|
318
|
+
}
|
|
319
|
+
// 4. target 트랙 업데이트 (commit은 withEdit 마지막에)
|
|
320
|
+
const updated = await publisher().edits.tracks.update({
|
|
321
|
+
auth, packageName, editId, track: toTrack,
|
|
322
|
+
requestBody: { track: toTrack, releases: mergedReleases },
|
|
323
|
+
});
|
|
324
|
+
return {
|
|
325
|
+
packageName,
|
|
326
|
+
fromTrack,
|
|
327
|
+
toTrack,
|
|
328
|
+
versionCode: String(versionCode),
|
|
329
|
+
newStatus: status,
|
|
330
|
+
userFraction: status === 'inProgress' ? userFraction : undefined,
|
|
331
|
+
releaseName: newRelease.name,
|
|
332
|
+
releaseNotesLanguages: (newRelease.releaseNotes ?? []).map((n) => n.language),
|
|
333
|
+
committed: true,
|
|
334
|
+
result: updated.data,
|
|
335
|
+
};
|
|
336
|
+
}, true);
|
|
337
|
+
}
|
|
265
338
|
export async function listReviews(auth, packageName) {
|
|
266
339
|
const res = await publisher().reviews.list({ auth, packageName });
|
|
267
340
|
return (res.data.reviews ?? []).map((r) => ({
|
package/package.json
CHANGED