@yoonion/mimi-seed-mcp 0.1.0 → 0.3.0

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.
@@ -1,14 +1,24 @@
1
1
  import { google } from 'googleapis';
2
2
  import { JWT } from 'google-auth-library';
3
+ import fs from 'node:fs';
4
+ function mimeTypeFor(filePath) {
5
+ const ext = filePath.toLowerCase().split('.').pop() ?? '';
6
+ if (ext === 'png')
7
+ return 'image/png';
8
+ if (ext === 'jpg' || ext === 'jpeg')
9
+ return 'image/jpeg';
10
+ if (ext === 'webp')
11
+ return 'image/webp';
12
+ return 'application/octet-stream';
13
+ }
3
14
  /**
4
15
  * Google Play Developer API (Android Publisher API v3) 래퍼
5
16
  *
6
17
  * 주의: 최초 앱 생성은 API로 불가 (Play Console에서만).
7
18
  * 여기서는 기존 앱의 메타데이터, 빌드, 출시를 관리.
8
19
  */
9
- const publisher = () => google.androidpublisher('v3');
10
- // ─── withEdit 헬퍼 ───
11
- async function withEdit(auth, packageName, fn, commit = false) {
20
+ export const publisher = () => google.androidpublisher('v3');
21
+ export async function withEdit(auth, packageName, fn, commit = false) {
12
22
  const res = await publisher().edits.insert({ auth, packageName });
13
23
  const editId = res.data.id;
14
24
  if (!editId)
@@ -81,6 +91,135 @@ export async function listTracks(auth, packageName) {
81
91
  }));
82
92
  });
83
93
  }
94
+ // ─── 릴리스 노트 업데이트 ───
95
+ //
96
+ // track의 특정 release(versionCode 매칭)에 대해 releaseNotes[language]를
97
+ // 교체/추가. 다른 언어 노트와 다른 release entry는 보존. edit 세션으로
98
+ // tracks.get → 수정 → tracks.update → commit. 이미 라이브(completed) 상태인
99
+ // release도 releaseNotes만은 편집 가능.
100
+ export async function updateReleaseNotes(auth, packageName, track, versionCode, language, text) {
101
+ return withEdit(auth, packageName, async (editId) => {
102
+ const current = await publisher().edits.tracks.get({
103
+ auth, packageName, editId, track,
104
+ });
105
+ const releases = current.data.releases ?? [];
106
+ if (releases.length === 0) {
107
+ throw new Error(`${track} 트랙에 릴리스가 없어.`);
108
+ }
109
+ const target = releases.find((r) => (r.versionCodes ?? []).some((v) => String(v) === String(versionCode)));
110
+ if (!target) {
111
+ const available = releases.map((r) => ({
112
+ name: r.name,
113
+ versionCodes: r.versionCodes,
114
+ status: r.status,
115
+ }));
116
+ throw new Error(`versionCode "${versionCode}"를 ${track} 트랙에서 찾을 수 없어. 가능한 릴리스: ${JSON.stringify(available)}`);
117
+ }
118
+ const notes = target.releaseNotes ?? [];
119
+ const idx = notes.findIndex((n) => n.language === language);
120
+ if (idx >= 0)
121
+ notes[idx] = { language, text };
122
+ else
123
+ notes.push({ language, text });
124
+ target.releaseNotes = notes;
125
+ const updated = await publisher().edits.tracks.update({
126
+ auth, packageName, editId, track,
127
+ requestBody: { track, releases },
128
+ });
129
+ return updated.data;
130
+ }, true);
131
+ }
132
+ /**
133
+ * 최신 release (versionCode 최대값)의 releaseNotes[language]를 교체/추가.
134
+ * versionCode를 모를 때 편의용.
135
+ */
136
+ export async function updateLatestReleaseNotes(auth, packageName, track, language, text) {
137
+ return withEdit(auth, packageName, async (editId) => {
138
+ const current = await publisher().edits.tracks.get({
139
+ auth, packageName, editId, track,
140
+ });
141
+ const releases = current.data.releases ?? [];
142
+ if (releases.length === 0) {
143
+ throw new Error(`${track} 트랙에 릴리스가 없어.`);
144
+ }
145
+ const maxVc = (r) => Math.max(...(r.versionCodes ?? []).map((v) => Number(v)), 0);
146
+ const target = releases.reduce((best, r) => (maxVc(r) > maxVc(best) ? r : best));
147
+ const notes = target.releaseNotes ?? [];
148
+ const idx = notes.findIndex((n) => n.language === language);
149
+ if (idx >= 0)
150
+ notes[idx] = { language, text };
151
+ else
152
+ notes.push({ language, text });
153
+ target.releaseNotes = notes;
154
+ const updated = await publisher().edits.tracks.update({
155
+ auth, packageName, editId, track,
156
+ requestBody: { track, releases },
157
+ });
158
+ return {
159
+ ...updated.data,
160
+ updatedVersionCodes: target.versionCodes,
161
+ updatedReleaseName: target.name,
162
+ };
163
+ }, true);
164
+ }
165
+ // ─── 이미지 (feature graphic / phone screenshots / etc.) ───
166
+ export async function listImages(auth, packageName, language, imageType) {
167
+ return withEdit(auth, packageName, async (editId) => {
168
+ const res = await publisher().edits.images.list({
169
+ auth, packageName, editId, language, imageType,
170
+ });
171
+ return res.data.images ?? [];
172
+ });
173
+ }
174
+ export async function uploadImage(auth, packageName, language, imageType, filePath) {
175
+ if (!fs.existsSync(filePath))
176
+ throw new Error(`File not found: ${filePath}`);
177
+ return withEdit(auth, packageName, async (editId) => {
178
+ const res = await publisher().edits.images.upload({
179
+ auth, packageName, editId, language, imageType,
180
+ media: {
181
+ mimeType: mimeTypeFor(filePath),
182
+ body: fs.createReadStream(filePath),
183
+ },
184
+ });
185
+ return res.data.image;
186
+ }, true);
187
+ }
188
+ export async function deleteAllImages(auth, packageName, language, imageType) {
189
+ return withEdit(auth, packageName, async (editId) => {
190
+ await publisher().edits.images.deleteall({
191
+ auth, packageName, editId, language, imageType,
192
+ });
193
+ return { ok: true, imageType };
194
+ }, true);
195
+ }
196
+ /**
197
+ * 한 edit 세션 내에서 기존 이미지 전체 삭제 + 새 이미지 순서대로 업로드 + commit.
198
+ * phoneScreenshots처럼 여러 장 교체 시 효율적 (단일 edit).
199
+ */
200
+ export async function replaceImages(auth, packageName, language, imageType, filePaths) {
201
+ for (const p of filePaths) {
202
+ if (!fs.existsSync(p))
203
+ throw new Error(`File not found: ${p}`);
204
+ }
205
+ return withEdit(auth, packageName, async (editId) => {
206
+ await publisher().edits.images.deleteall({
207
+ auth, packageName, editId, language, imageType,
208
+ });
209
+ const uploaded = [];
210
+ for (const filePath of filePaths) {
211
+ const res = await publisher().edits.images.upload({
212
+ auth, packageName, editId, language, imageType,
213
+ media: {
214
+ mimeType: mimeTypeFor(filePath),
215
+ body: fs.createReadStream(filePath),
216
+ },
217
+ });
218
+ uploaded.push(res.data.image ?? {});
219
+ }
220
+ return { imageType, count: uploaded.length, uploaded };
221
+ }, true);
222
+ }
84
223
  // ─── 리뷰 조회 ───
85
224
  export async function listReviews(auth, packageName) {
86
225
  const res = await publisher().reviews.list({ auth, packageName });
@@ -107,16 +246,15 @@ export async function replyToReview(auth, packageName, reviewId, replyText) {
107
246
  }
108
247
  // ─── 인앱 상품 조회 ───
109
248
  export async function listInAppProducts(auth, packageName) {
110
- const res = await publisher().inappproducts.list({
249
+ const res = await publisher().monetization.onetimeproducts.list({
111
250
  auth,
112
251
  packageName,
252
+ pageSize: 100,
113
253
  });
114
- return (res.data.inappproduct ?? []).map((p) => ({
115
- sku: p.sku,
116
- status: p.status,
117
- purchaseType: p.purchaseType,
118
- defaultPrice: p.defaultPrice,
254
+ return (res.data.oneTimeProducts ?? []).map((p) => ({
255
+ productId: p.productId,
119
256
  listings: p.listings,
257
+ purchaseOptions: p.purchaseOptions,
120
258
  }));
121
259
  }
122
260
  // ─── 구독 조회 ───
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yoonion/mimi-seed-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Mimi Seed MCP server — Firebase + AdMob + Google Play + App Store management for Claude Code / Cursor / any MCP client.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,8 +42,9 @@
42
42
  "node": ">=20"
43
43
  },
44
44
  "dependencies": {
45
+ "@anthropic-ai/sdk": "^0.52.0",
45
46
  "@modelcontextprotocol/sdk": "^1.12.1",
46
- "googleapis": "^148.0.0",
47
+ "googleapis": "^171.4.0",
47
48
  "jsonwebtoken": "^9.0.3",
48
49
  "open": "^10.1.0",
49
50
  "zod": "^3.24.0"