mimi-seed 0.2.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.
Files changed (3) hide show
  1. package/README.md +173 -0
  2. package/dist/index.js +847 -0
  3. package/package.json +33 -0
package/README.md ADDED
@@ -0,0 +1,173 @@
1
+ # mimi-seed
2
+
3
+ **Mimi Seed CLI** — Claude Code에서 앱 출시를 자동화하는 커맨드라인 도구.
4
+
5
+ git log에서 릴리즈 노트를 생성하고, 출시 전 위험 요소를 자동 점검하며, Play Store / App Store에 바로 적용합니다.
6
+
7
+ ## 빠른 시작
8
+
9
+ ```bash
10
+ npx mimi-seed init
11
+ ```
12
+
13
+ 현재 디렉토리에서 Android/iOS 앱을 자동 감지해 Mimi Seed 워크스페이스에 등록하고, PAT를 `~/.mimi-seed/config.json`에 저장합니다.
14
+
15
+ ## 명령어
16
+
17
+ | 명령어 | 설명 |
18
+ |--------|------|
19
+ | `mimi-seed init` | 프로젝트를 Mimi Seed에 연결 (PAT 발급 + 앱 자동 등록) |
20
+ | `mimi-seed status` | 연결 상태 + 등록 앱 목록 |
21
+ | `mimi-seed doctor` | 환경 진단 (토큰·Git·앱·CI 한 번에 체크) |
22
+ | `mimi-seed check` | 출시 전 Readiness 점검 (점수 + 블로커) |
23
+ | `mimi-seed notes` | AI 릴리즈 노트 생성 (git log → 3 톤 → 다국어 → 적용) |
24
+ | `mimi-seed logout` | 로컬 설정 삭제 |
25
+
26
+ ---
27
+
28
+ ## mimi-seed notes
29
+
30
+ git 커밋 내역으로 앱 스토어 릴리즈 노트를 자동 생성합니다.
31
+
32
+ ```bash
33
+ # 기본: 최신 태그 이후 커밋 → 간결/상세/마케팅 3 톤 생성
34
+ mimi-seed notes
35
+
36
+ # 태그 범위 지정
37
+ mimi-seed notes --from v1.2.0 --to HEAD
38
+
39
+ # 다국어 동시 생성 (AI 필요)
40
+ mimi-seed notes --locale ko,en-US,ja
41
+
42
+ # 생성 후 Play Store 바로 적용
43
+ mimi-seed notes --apply
44
+
45
+ # CI 모드 (프롬프트 없음)
46
+ mimi-seed notes --no-interactive --apply
47
+ ```
48
+
49
+ **AI 생성 활성화** (`ANTHROPIC_API_KEY` 설정 시):
50
+ ```bash
51
+ export ANTHROPIC_API_KEY=sk-ant-...
52
+ mimi-seed notes --locale ko,en-US,ja
53
+ ```
54
+
55
+ 설정하지 않으면 커밋 메시지 자동 포맷팅으로 동작합니다.
56
+
57
+ ### 옵션
58
+
59
+ | 옵션 | 기본값 | 설명 |
60
+ |------|--------|------|
61
+ | `--from <ref>` | 최신 태그 | 시작 커밋 또는 태그 |
62
+ | `--to <ref>` | `HEAD` | 끝 커밋 |
63
+ | `--locale <list>` | `ko,en-US` | 다국어 로케일 (쉼표 구분) |
64
+ | `--apply` | false | 생성 후 스토어에 바로 적용 |
65
+ | `--no-interactive` | false | CI 모드 (프롬프트 없음) |
66
+ | `--limit <n>` | 30 | 최대 커밋 수 |
67
+
68
+ ---
69
+
70
+ ## mimi-seed check
71
+
72
+ 출시 전 Readiness 점수와 블로커를 확인합니다.
73
+
74
+ ```bash
75
+ mimi-seed check
76
+
77
+ # CI에서 블로커 있으면 exit 1
78
+ mimi-seed check --fail-on-blocker
79
+ ```
80
+
81
+ ### 옵션
82
+
83
+ | 옵션 | 설명 |
84
+ |------|------|
85
+ | `--app <id>` | 앱 ID 지정 (기본: 첫 번째 등록 앱) |
86
+ | `--fail-on-blocker` | 블로커 존재 시 exit 1 (CI/CD용) |
87
+
88
+ ---
89
+
90
+ ## mimi-seed doctor
91
+
92
+ 로컬 환경 전체를 진단합니다.
93
+
94
+ ```bash
95
+ mimi-seed doctor
96
+ # ✓ 토큰 저장됨 prs_abc1... (2026-04-25)
97
+ # ✓ 엔드포인트 https://mimi-seed.pryzm.gg/api/mcp
98
+ # ✓ Mimi Seed 서버 연결됨 앱 2개
99
+ #
100
+ # ── 로컬 환경 ──
101
+ # ✓ Node.js v22.17.0
102
+ # ✓ Git 저장소 최신 태그: v1.3.0
103
+ # ⚠ ANTHROPIC_API_KEY 없음 설정 시 AI 릴리즈 노트/리뷰 답변 생성 가능
104
+ #
105
+ # ── 앱 감지 ──
106
+ # ✓ MyApp android:com.example.myapp ios:com.example.myapp
107
+ ```
108
+
109
+ ---
110
+
111
+ ## CI/CD 사용
112
+
113
+ 브라우저 없이 환경변수만으로 실행합니다.
114
+
115
+ ```yaml
116
+ # GitHub Actions 예시
117
+ - name: Release notes & readiness check
118
+ env:
119
+ MIMI_SEED_TOKEN: ${{ secrets.MIMI_SEED_TOKEN }}
120
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
121
+ run: |
122
+ npx mimi-seed notes --apply --no-interactive --locale ko,en-US
123
+ npx mimi-seed check --fail-on-blocker
124
+ ```
125
+
126
+ `MIMI_SEED_TOKEN`은 [대시보드 → API 토큰](https://mimi-seed.pryzm.gg/workspace/api-tokens)에서 발급하세요.
127
+
128
+ ---
129
+
130
+ ## Claude Code MCP 등록
131
+
132
+ `init` 후 1회 실행:
133
+
134
+ ```bash
135
+ claude mcp add --transport http mimi-seed https://mimi-seed.pryzm.gg/api/mcp \
136
+ --header "Authorization: Bearer <PAT>"
137
+ ```
138
+
139
+ 등록 후 Claude Code에서 대화로 제어:
140
+
141
+ ```
142
+ "내 앱 출시 준비됐어?"
143
+ "릴리즈 노트 써줘"
144
+ "스크린샷 검수해줘"
145
+ ```
146
+
147
+ ---
148
+
149
+ ## 앱 감지 대상
150
+
151
+ - `app.json` / `app.config.json` (Expo, React Native)
152
+ - `**/build.gradle(.kts)` — `applicationId`
153
+ - `**/Info.plist` — `CFBundleIdentifier`
154
+ - `**/project.pbxproj` — `PRODUCT_BUNDLE_IDENTIFIER`
155
+ - `package.json` — 앱 이름 보충
156
+
157
+ ---
158
+
159
+ ## 환경변수
160
+
161
+ | 변수 | 설명 |
162
+ |------|------|
163
+ | `MIMI_SEED_TOKEN` | PAT 토큰 — CI/CD 무인증 모드 |
164
+ | `MIMI_SEED_WEB_BASE` | 서버 주소 (기본: `https://mimi-seed.pryzm.gg`) |
165
+ | `ANTHROPIC_API_KEY` | AI 릴리즈 노트/리뷰 답변 생성 활성화 (선택) |
166
+ | `DEBUG` | `1` 설정 시 오류 스택 트레이스 출력 |
167
+
168
+ ---
169
+
170
+ ## 관련 패키지
171
+
172
+ - [`@yoonion/mimi-seed-mcp`](https://www.npmjs.com/package/@yoonion/mimi-seed-mcp) — Claude Desktop / Cursor용 로컬 MCP 서버 (Firebase · Play Store · App Store · AdMob)
173
+ - [mimi-seed.pryzm.gg](https://mimi-seed.pryzm.gg) — 웹 콘솔 (스크린샷 검수, Copy Studio, 팀 워크스페이스)
package/dist/index.js ADDED
@@ -0,0 +1,847 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import os2 from "os";
5
+ import kleur4 from "kleur";
6
+ import open from "open";
7
+
8
+ // src/detect.ts
9
+ import fs from "fs/promises";
10
+ import path from "path";
11
+ async function readIfExists(p) {
12
+ try {
13
+ return await fs.readFile(p, "utf8");
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+ async function pathExists(p) {
19
+ try {
20
+ await fs.access(p);
21
+ return true;
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+ async function walk(root, match, maxDepth = 5) {
27
+ const found = [];
28
+ const skipDirs = /* @__PURE__ */ new Set([
29
+ "node_modules",
30
+ ".git",
31
+ "build",
32
+ "dist",
33
+ ".next",
34
+ ".expo",
35
+ "Pods",
36
+ "DerivedData"
37
+ ]);
38
+ async function visit(dir, depth) {
39
+ if (depth > maxDepth) return;
40
+ let entries;
41
+ try {
42
+ entries = await fs.readdir(dir, { withFileTypes: true });
43
+ } catch {
44
+ return;
45
+ }
46
+ for (const e of entries) {
47
+ if (e.isDirectory()) {
48
+ if (skipDirs.has(e.name)) continue;
49
+ await visit(path.join(dir, e.name), depth + 1);
50
+ } else if (e.isFile() && match(e.name)) {
51
+ found.push(path.join(dir, e.name));
52
+ }
53
+ }
54
+ }
55
+ await visit(root, 0);
56
+ return found;
57
+ }
58
+ async function detectHints(cwd) {
59
+ const hints = [];
60
+ for (const fname of ["app.json", "app.config.json"]) {
61
+ const txt = await readIfExists(path.join(cwd, fname));
62
+ if (!txt) continue;
63
+ try {
64
+ const json = JSON.parse(txt);
65
+ const expo = json.expo ?? json;
66
+ const pkg = expo?.android?.package;
67
+ const bid = expo?.ios?.bundleIdentifier;
68
+ const name = expo?.name;
69
+ if (pkg || bid) {
70
+ hints.push({
71
+ name,
72
+ packageName: typeof pkg === "string" ? pkg : void 0,
73
+ bundleId: typeof bid === "string" ? bid : void 0,
74
+ source: [fname]
75
+ });
76
+ }
77
+ } catch {
78
+ }
79
+ }
80
+ const gradleFiles = await walk(
81
+ cwd,
82
+ (n) => n === "build.gradle" || n === "build.gradle.kts",
83
+ 4
84
+ );
85
+ for (const f of gradleFiles) {
86
+ const txt = await readIfExists(f);
87
+ if (!txt) continue;
88
+ const m = txt.match(/applicationId[\s=]+["']([^"']+)["']/);
89
+ if (m?.[1]) {
90
+ const pkg = m[1];
91
+ if (!hints.some((h) => h.packageName === pkg)) {
92
+ hints.push({ packageName: pkg, source: [path.relative(cwd, f)] });
93
+ }
94
+ }
95
+ }
96
+ const plistFiles = await walk(cwd, (n) => n === "Info.plist", 5);
97
+ for (const f of plistFiles) {
98
+ const txt = await readIfExists(f);
99
+ if (!txt) continue;
100
+ const m = txt.match(
101
+ /<key>CFBundleIdentifier<\/key>\s*<string>([^<]+)<\/string>/
102
+ );
103
+ if (m?.[1]) {
104
+ let bid = m[1];
105
+ if (bid.includes("$(PRODUCT_BUNDLE_IDENTIFIER)")) continue;
106
+ if (!hints.some((h) => h.bundleId === bid)) {
107
+ hints.push({ bundleId: bid, source: [path.relative(cwd, f)] });
108
+ }
109
+ }
110
+ }
111
+ const pbxFiles = await walk(cwd, (n) => n === "project.pbxproj", 5);
112
+ for (const f of pbxFiles) {
113
+ const txt = await readIfExists(f);
114
+ if (!txt) continue;
115
+ const matches = [...txt.matchAll(/PRODUCT_BUNDLE_IDENTIFIER = ([^;]+);/g)];
116
+ for (const m of matches) {
117
+ const bid = m[1].trim().replace(/^["']|["']$/g, "");
118
+ if (!bid || bid.includes("$")) continue;
119
+ if (!hints.some((h) => h.bundleId === bid)) {
120
+ hints.push({ bundleId: bid, source: [path.relative(cwd, f)] });
121
+ }
122
+ }
123
+ }
124
+ const pkgJson = await readIfExists(path.join(cwd, "package.json"));
125
+ if (pkgJson) {
126
+ try {
127
+ const json = JSON.parse(pkgJson);
128
+ if (typeof json.name === "string") {
129
+ for (const h of hints) if (!h.name) h.name = json.name;
130
+ }
131
+ } catch {
132
+ }
133
+ }
134
+ const merged = [];
135
+ const androidOnly = hints.filter((h) => h.packageName && !h.bundleId);
136
+ const iosOnly = hints.filter((h) => h.bundleId && !h.packageName);
137
+ const both = hints.filter((h) => h.packageName && h.bundleId);
138
+ merged.push(...both);
139
+ for (const a of androidOnly) {
140
+ const match = iosOnly.find((i) => i.bundleId === a.packageName);
141
+ if (match) {
142
+ merged.push({
143
+ name: a.name ?? match.name,
144
+ packageName: a.packageName,
145
+ bundleId: match.bundleId,
146
+ source: [...a.source, ...match.source]
147
+ });
148
+ } else {
149
+ merged.push(a);
150
+ }
151
+ }
152
+ for (const i of iosOnly) {
153
+ if (!androidOnly.some((a) => a.packageName === i.bundleId)) {
154
+ merged.push(i);
155
+ }
156
+ }
157
+ return merged.filter((h) => h.packageName || h.bundleId);
158
+ }
159
+ async function hasAnyProjectSignal(cwd) {
160
+ return await pathExists(path.join(cwd, "package.json")) || await pathExists(path.join(cwd, "app.json")) || await pathExists(path.join(cwd, "android")) || await pathExists(path.join(cwd, "ios"));
161
+ }
162
+
163
+ // src/handshake.ts
164
+ import http from "http";
165
+ async function awaitHandshake(timeoutMs) {
166
+ let resolve;
167
+ let reject;
168
+ const promise = new Promise((res, rej) => {
169
+ resolve = res;
170
+ reject = rej;
171
+ });
172
+ const server = http.createServer((req, res) => {
173
+ if (!req.url) {
174
+ res.statusCode = 400;
175
+ res.end();
176
+ return;
177
+ }
178
+ const url = new URL(req.url, "http://localhost");
179
+ if (url.pathname !== "/cb") {
180
+ res.statusCode = 404;
181
+ res.end();
182
+ return;
183
+ }
184
+ const token = url.searchParams.get("token");
185
+ const prefix = url.searchParams.get("prefix") ?? "";
186
+ if (!token) {
187
+ res.statusCode = 400;
188
+ res.end("token missing");
189
+ return;
190
+ }
191
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
192
+ res.end(`<!doctype html>
193
+ <html lang="ko"><body style="font-family:system-ui;padding:40px;max-width:480px;margin:auto">
194
+ <h2>\u2713 Mimi Seed \uC5F0\uACB0 \uC644\uB8CC</h2>
195
+ <p>\uD130\uBBF8\uB110\uB85C \uB3CC\uC544\uAC00\uC138\uC694.</p>
196
+ <script>setTimeout(()=>window.close(),1000)</script>
197
+ </body></html>`);
198
+ resolve({ token, prefix });
199
+ setTimeout(() => server.close(), 500);
200
+ });
201
+ server.on("error", reject);
202
+ await new Promise((res) => server.listen(0, "127.0.0.1", res));
203
+ const port = server.address().port;
204
+ const timer = setTimeout(() => {
205
+ server.close();
206
+ reject(new Error(`${timeoutMs / 1e3}\uCD08 \uC548\uC5D0 \uC5F0\uACB0\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.`));
207
+ }, timeoutMs);
208
+ promise.finally(() => clearTimeout(timer));
209
+ return { port, promise };
210
+ }
211
+
212
+ // src/mcp-client.ts
213
+ async function mcpCall(endpoint, token, name, args) {
214
+ const res = await fetch(endpoint, {
215
+ method: "POST",
216
+ headers: {
217
+ "Content-Type": "application/json",
218
+ Accept: "application/json, text/event-stream",
219
+ Authorization: `Bearer ${token}`
220
+ },
221
+ body: JSON.stringify({
222
+ jsonrpc: "2.0",
223
+ id: 1,
224
+ method: "tools/call",
225
+ params: { name, arguments: args }
226
+ })
227
+ });
228
+ const contentType = res.headers.get("content-type") ?? "";
229
+ let payload;
230
+ if (contentType.includes("text/event-stream")) {
231
+ const text2 = await res.text();
232
+ const line = text2.split("\n").map((l) => l.trim()).find((l) => l.startsWith("data:"));
233
+ if (!line) throw new Error("MCP SSE \uC751\uB2F5\uC5D0 data \uC5C6\uC74C");
234
+ payload = JSON.parse(line.slice(5).trim());
235
+ } else {
236
+ payload = await res.json();
237
+ }
238
+ if (payload.error) {
239
+ return { text: payload.error.message, isError: true };
240
+ }
241
+ const content = payload.result?.content ?? [];
242
+ const text = content.filter((c) => c.type === "text").map((c) => c.text ?? "").join("\n");
243
+ return { text, isError: payload.result?.isError ?? false };
244
+ }
245
+
246
+ // src/config.ts
247
+ import fs2 from "fs/promises";
248
+ import path2 from "path";
249
+ import os from "os";
250
+ var CONFIG_DIR = path2.join(os.homedir(), ".mimi-seed");
251
+ var CONFIG_PATH = path2.join(CONFIG_DIR, "config.json");
252
+ async function readConfig() {
253
+ try {
254
+ const txt = await fs2.readFile(CONFIG_PATH, "utf8");
255
+ return JSON.parse(txt);
256
+ } catch {
257
+ return null;
258
+ }
259
+ }
260
+ async function writeConfig(cfg) {
261
+ await fs2.mkdir(CONFIG_DIR, { recursive: true });
262
+ await fs2.writeFile(CONFIG_PATH, JSON.stringify(cfg, null, 2), { mode: 384 });
263
+ }
264
+ async function deleteConfig() {
265
+ await fs2.rm(CONFIG_PATH, { force: true });
266
+ }
267
+ var CONFIG_LOCATION = CONFIG_PATH;
268
+ async function getEffectiveConfig() {
269
+ const envToken = process.env.MIMI_SEED_TOKEN;
270
+ if (envToken) {
271
+ const webBase = process.env.MIMI_SEED_WEB_BASE ?? "https://mimi-seed.pryzm.gg";
272
+ return {
273
+ token: envToken,
274
+ prefix: envToken.slice(0, 8),
275
+ endpoint: `${webBase}/api/mcp`,
276
+ webBase,
277
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
278
+ };
279
+ }
280
+ return readConfig();
281
+ }
282
+
283
+ // src/doctor.ts
284
+ import kleur from "kleur";
285
+
286
+ // src/git.ts
287
+ import { execSync } from "child_process";
288
+ function isGitRepo(cwd) {
289
+ try {
290
+ execSync("git rev-parse --git-dir", { cwd, stdio: "pipe" });
291
+ return true;
292
+ } catch {
293
+ return false;
294
+ }
295
+ }
296
+ function getLatestTag(cwd) {
297
+ try {
298
+ return execSync("git describe --tags --abbrev=0", { cwd, stdio: "pipe" }).toString().trim();
299
+ } catch {
300
+ return null;
301
+ }
302
+ }
303
+ function getGitLog(cwd, opts = {}) {
304
+ const { from, to = "HEAD", limit = 30 } = opts;
305
+ const range = from ? `${from}..${to}` : to;
306
+ const format = "%H%s%ci%an";
307
+ const limitFlag = `--max-count=${limit}`;
308
+ let out;
309
+ try {
310
+ out = execSync(`git log ${limitFlag} --format="${format}" ${range}`, {
311
+ cwd,
312
+ stdio: "pipe"
313
+ }).toString();
314
+ } catch {
315
+ return [];
316
+ }
317
+ return out.split("\n").filter((l) => l.trim()).map((line) => {
318
+ const [hash, message, date, author] = line.split("");
319
+ return { hash: hash?.slice(0, 8) ?? "", message: message ?? "", date: date ?? "", author: author ?? "" };
320
+ }).filter((c) => c.hash && c.message);
321
+ }
322
+ function formatCommitsForPrompt(commits) {
323
+ return commits.map((c) => `- ${c.message} (${c.author}, ${c.date.slice(0, 10)})`).join("\n");
324
+ }
325
+
326
+ // src/doctor.ts
327
+ function ok(label, detail = "") {
328
+ process.stdout.write(` ${kleur.green("\u2713")} ${label}${detail ? kleur.dim(" " + detail) : ""}
329
+ `);
330
+ }
331
+ function warn(label, detail = "") {
332
+ process.stdout.write(` ${kleur.yellow("\u26A0")} ${label}${detail ? kleur.dim(" " + detail) : ""}
333
+ `);
334
+ }
335
+ function fail(label, detail = "") {
336
+ process.stdout.write(` ${kleur.red("\u2717")} ${label}${detail ? kleur.dim(" " + detail) : ""}
337
+ `);
338
+ }
339
+ function section(title) {
340
+ process.stdout.write("\n" + kleur.dim(`\u2500\u2500 ${title} \u2500\u2500
341
+ `));
342
+ }
343
+ async function cmdDoctor() {
344
+ const cwd = process.cwd();
345
+ process.stdout.write(kleur.bold("mimi-seed doctor\n\n"));
346
+ section("\uC778\uC99D");
347
+ const cfg = await getEffectiveConfig();
348
+ if (!cfg) {
349
+ fail("Mimi Seed \uD1A0\uD070 \uC5C6\uC74C", "`mimi-seed init` \uC2E4\uD589 \uD544\uC694");
350
+ } else {
351
+ ok("\uD1A0\uD070 \uC800\uC7A5\uB428", `${cfg.prefix}\u2026 (${cfg.createdAt.slice(0, 10)})`);
352
+ ok("\uC5D4\uB4DC\uD3EC\uC778\uD2B8", cfg.endpoint);
353
+ if (process.env.MIMI_SEED_TOKEN) {
354
+ ok("CI \uBAA8\uB4DC", "MIMI_SEED_TOKEN \uD658\uACBD\uBCC0\uC218 \uC0AC\uC6A9 \uC911");
355
+ }
356
+ const r = await mcpCall(cfg.endpoint, cfg.token, "list_apps", {});
357
+ if (r.isError) {
358
+ fail("\uD1A0\uD070 \uAC80\uC99D \uC2E4\uD328", r.text.slice(0, 80));
359
+ } else {
360
+ const lines = r.text.split("\n").filter(Boolean);
361
+ ok("Mimi Seed \uC11C\uBC84 \uC5F0\uACB0\uB428", `\uC571 ${lines.length}\uAC1C`);
362
+ }
363
+ }
364
+ section("\uB85C\uCEEC \uD658\uACBD");
365
+ const nodeVer = process.version;
366
+ const [, major] = nodeVer.match(/v(\d+)/) ?? [];
367
+ if (Number(major) >= 18) {
368
+ ok("Node.js", nodeVer);
369
+ } else {
370
+ fail("Node.js", `${nodeVer} \u2014 v18 \uC774\uC0C1 \uD544\uC694`);
371
+ }
372
+ if (isGitRepo(cwd)) {
373
+ const latestTag = getLatestTag(cwd);
374
+ const commits = getGitLog(cwd, { limit: 5 });
375
+ ok("Git \uC800\uC7A5\uC18C", latestTag ? `\uCD5C\uC2E0 \uD0DC\uADF8: ${latestTag}` : `\uCEE4\uBC0B ${commits.length}\uAC1C`);
376
+ } else {
377
+ warn("Git \uC800\uC7A5\uC18C \uC5C6\uC74C", "mimi-seed notes \uC0AC\uC6A9 \uBD88\uAC00");
378
+ }
379
+ if (process.env.ANTHROPIC_API_KEY) {
380
+ ok("ANTHROPIC_API_KEY", "AI \uB9B4\uB9AC\uC988 \uB178\uD2B8 \uC0DD\uC131 \uC0AC\uC6A9 \uAC00\uB2A5");
381
+ } else {
382
+ warn("ANTHROPIC_API_KEY \uC5C6\uC74C", "\uC124\uC815 \uC2DC AI \uB9B4\uB9AC\uC988 \uB178\uD2B8/\uB9AC\uBDF0 \uB2F5\uBCC0 \uC0DD\uC131 \uAC00\uB2A5");
383
+ }
384
+ section("\uC571 \uAC10\uC9C0");
385
+ const hints = await detectHints(cwd);
386
+ if (hints.length === 0) {
387
+ warn("\uC571 \uAC10\uC9C0 \uC5C6\uC74C", "app.json / build.gradle / Info.plist \uC5C6\uC74C");
388
+ } else {
389
+ for (const h of hints) {
390
+ const ids = [h.packageName && `android:${h.packageName}`, h.bundleId && `ios:${h.bundleId}`].filter(Boolean).join(" ");
391
+ ok(h.name ?? "(\uC774\uB984 \uBBF8\uC0C1)", ids);
392
+ }
393
+ }
394
+ process.stdout.write("\n");
395
+ }
396
+
397
+ // src/check.ts
398
+ import kleur2 from "kleur";
399
+ function parseArgs(argv) {
400
+ const args = { failOnBlocker: false };
401
+ for (let i = 0; i < argv.length; i++) {
402
+ if (argv[i] === "--app" && argv[i + 1]) args.appId = argv[++i];
403
+ if (argv[i] === "--fail-on-blocker") args.failOnBlocker = true;
404
+ }
405
+ return args;
406
+ }
407
+ function renderScore(score) {
408
+ const filled = Math.round(score / 5);
409
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(20 - filled);
410
+ const color = score >= 80 ? kleur2.green : score >= 50 ? kleur2.yellow : kleur2.red;
411
+ return color(`${bar} ${score}/100`);
412
+ }
413
+ var MODULE_LABELS = {
414
+ integration: "Integration",
415
+ copy: "Copy Studio",
416
+ screenshot: "Screenshot",
417
+ checklist: "Checklist"
418
+ };
419
+ async function cmdCheck(argv) {
420
+ const args = parseArgs(argv);
421
+ const cfg = await getEffectiveConfig();
422
+ if (!cfg) {
423
+ process.stdout.write(kleur2.red("\uC5F0\uACB0\uB41C \uACC4\uC815 \uC5C6\uC74C. `mimi-seed init` \uC2E4\uD589.\n"));
424
+ process.exit(1);
425
+ }
426
+ process.stdout.write(kleur2.bold("mimi-seed check \u2014 \uCD9C\uC2DC \uC804 \uC810\uAC80\n\n"));
427
+ const appsResult = await mcpCall(cfg.endpoint, cfg.token, "list_apps", {});
428
+ if (appsResult.isError) {
429
+ process.stdout.write(kleur2.red(`\uC571 \uBAA9\uB85D \uC870\uD68C \uC2E4\uD328: ${appsResult.text}
430
+ `));
431
+ process.exit(1);
432
+ }
433
+ let appId = args.appId;
434
+ if (!appId) {
435
+ try {
436
+ const apps = JSON.parse(appsResult.text);
437
+ if (!Array.isArray(apps) || apps.length === 0) {
438
+ process.stdout.write(kleur2.yellow("\uB4F1\uB85D\uB41C \uC571\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. `mimi-seed init` \uD6C4 \uC571\uC744 \uB4F1\uB85D\uD558\uC138\uC694.\n"));
439
+ process.exit(0);
440
+ }
441
+ appId = apps[0].id;
442
+ process.stdout.write(kleur2.dim(`\uC571: ${apps[0].name ?? appId}
443
+
444
+ `));
445
+ } catch {
446
+ process.stdout.write(kleur2.red(`\uC571 \uBAA9\uB85D \uD30C\uC2F1 \uC2E4\uD328: ${appsResult.text.slice(0, 80)}
447
+ `));
448
+ process.exit(1);
449
+ }
450
+ }
451
+ process.stdout.write("\u{1F4CA} Readiness \uC810\uC218 \uACC4\uC0B0 \uC911...\n");
452
+ const readinessResult = await mcpCall(cfg.endpoint, cfg.token, "get_readiness", { app_id: appId });
453
+ if (readinessResult.isError) {
454
+ process.stdout.write(kleur2.red(`\uC810\uC218 \uC870\uD68C \uC2E4\uD328: ${readinessResult.text}
455
+ `));
456
+ process.exit(1);
457
+ }
458
+ let hasBlocker = false;
459
+ try {
460
+ const data = JSON.parse(readinessResult.text);
461
+ const score = data.score ?? 0;
462
+ process.stdout.write(`
463
+ \uC810\uC218: ${renderScore(score)}
464
+
465
+ `);
466
+ if (data.modules) {
467
+ process.stdout.write(kleur2.dim("\u2500\u2500 \uBAA8\uB4C8\uBCC4 \u2500\u2500\n"));
468
+ for (const [key, val] of Object.entries(data.modules)) {
469
+ const label = MODULE_LABELS[key] ?? key;
470
+ const color = val >= 25 ? kleur2.green : val >= 10 ? kleur2.yellow : kleur2.red;
471
+ process.stdout.write(` ${label.padEnd(12)} ${color(String(val).padStart(2))}/25
472
+ `);
473
+ }
474
+ process.stdout.write("\n");
475
+ }
476
+ if (data.blockers?.length) {
477
+ hasBlocker = true;
478
+ process.stdout.write(kleur2.bold("\u{1F6AB} \uBE14\uB85C\uCEE4:\n"));
479
+ for (const b of data.blockers) process.stdout.write(` ${kleur2.red("\u2022")} ${b}
480
+ `);
481
+ process.stdout.write("\n");
482
+ }
483
+ if (data.warnings?.length) {
484
+ process.stdout.write(kleur2.bold("\u26A0 \uACBD\uACE0:\n"));
485
+ for (const w of data.warnings) process.stdout.write(` ${kleur2.yellow("\u2022")} ${w}
486
+ `);
487
+ process.stdout.write("\n");
488
+ }
489
+ if (!hasBlocker) {
490
+ process.stdout.write(score >= 80 ? kleur2.green("\u2713 \uCD9C\uC2DC \uC900\uBE44 \uC644\uB8CC!\n") : kleur2.yellow("\uC810\uC218\uB97C \uB192\uC774\uB824\uBA74 \uB300\uC2DC\uBCF4\uB4DC\uB97C \uD655\uC778\uD558\uC138\uC694.\n"));
491
+ }
492
+ } catch {
493
+ for (const line of readinessResult.text.split("\n")) process.stdout.write(" " + line + "\n");
494
+ }
495
+ if (hasBlocker && args.failOnBlocker) process.exit(1);
496
+ }
497
+
498
+ // src/notes.ts
499
+ import { createInterface } from "readline/promises";
500
+ import Anthropic from "@anthropic-ai/sdk";
501
+ import kleur3 from "kleur";
502
+ function parseArgs2(argv) {
503
+ const args = { to: "HEAD", locales: ["ko", "en-US"], apply: false, noInteractive: false, limit: 30 };
504
+ for (let i = 0; i < argv.length; i++) {
505
+ if (argv[i] === "--from" && argv[i + 1]) args.from = argv[++i];
506
+ if (argv[i] === "--to" && argv[i + 1]) args.to = argv[++i];
507
+ if (argv[i] === "--locale" && argv[i + 1]) args.locales = argv[++i].split(",").map((l) => l.trim());
508
+ if (argv[i] === "--apply") args.apply = true;
509
+ if (argv[i] === "--no-interactive") args.noInteractive = true;
510
+ if (argv[i] === "--limit" && argv[i + 1]) args.limit = parseInt(argv[++i], 10);
511
+ }
512
+ return args;
513
+ }
514
+ async function promptUser(question) {
515
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
516
+ const answer = await rl.question(question);
517
+ rl.close();
518
+ return answer.trim();
519
+ }
520
+ async function generateWithClaude(commitsText, locales) {
521
+ const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
522
+ const localeList = locales.map((l) => `"${l}": "\uD574\uB2F9 \uC5B8\uC5B4\uB85C \uBC88\uC5ED\uB41C \uAC04\uACB0\uD55C \uBC84\uC804"`).join(",\n ");
523
+ const response = await client.messages.create({
524
+ model: "claude-haiku-4-5-20251001",
525
+ max_tokens: 1500,
526
+ system: "\uC571 \uC2A4\uD1A0\uC5B4 \uB9B4\uB9AC\uC988 \uB178\uD2B8 \uC804\uBB38 \uCE74\uD53C\uB77C\uC774\uD130\uC785\uB2C8\uB2E4. \uCEE4\uBC0B \uB0B4\uC5ED\uC744 \uC0AC\uC6A9\uC790 \uCE5C\uD654\uC801\uC778 \uC5B8\uC5B4\uB85C \uBCC0\uD658\uD569\uB2C8\uB2E4. \uD56D\uC0C1 \uC720\uD6A8\uD55C JSON\uC73C\uB85C\uB9CC \uC751\uB2F5\uD558\uC138\uC694.",
527
+ messages: [{
528
+ role: "user",
529
+ content: `\uB2E4\uC74C \uCEE4\uBC0B \uB0B4\uC5ED\uC73C\uB85C \uB9B4\uB9AC\uC988 \uB178\uD2B8\uB97C 3\uAC00\uC9C0 \uD1A4\uC73C\uB85C \uC791\uC131\uD558\uC138\uC694:
530
+
531
+ ${commitsText}
532
+
533
+ JSON:
534
+ {
535
+ "concise": "\uAC04\uACB0\uD55C \uBC84\uC804 (3\uC904 \uC774\uB0B4, \uBD88\uB9BF)",
536
+ "detailed": "\uC0C1\uC138 \uBC84\uC804 (5\uAC1C \uC774\uB0B4, \uBD88\uB9BF)",
537
+ "marketing": "\uB9C8\uCF00\uD305 \uBC84\uC804 (\uC5F4\uC815\uC801 \uD1A4)",
538
+ "localized": {
539
+ ${localeList}
540
+ }
541
+ }`
542
+ }]
543
+ });
544
+ const text = response.content[0].type === "text" ? response.content[0].text : "";
545
+ const match = text.match(/\{[\s\S]*\}/);
546
+ if (!match) throw new Error("AI \uC751\uB2F5 \uD30C\uC2F1 \uC2E4\uD328");
547
+ return JSON.parse(match[0]);
548
+ }
549
+ function generateTemplate(commits, locales) {
550
+ const items = commits.slice(0, 10).map((c) => {
551
+ const msg = c.message.replace(/^(feat|fix|chore|docs|refactor|style|test|perf|ci|build)(\([^)]+\))?:\s*/i, "").trim();
552
+ return `\u2022 ${msg.charAt(0).toUpperCase() + msg.slice(1)}`;
553
+ });
554
+ const concise = items.slice(0, 3).join("\n");
555
+ const detailed = items.join("\n");
556
+ const marketing = `\uC0C8\uB85C\uC6B4 \uC5C5\uB370\uC774\uD2B8\uAC00 \uC900\uBE44\uB410\uC2B5\uB2C8\uB2E4!
557
+
558
+ ${items.slice(0, 5).join("\n")}
559
+
560
+ \uC9C0\uAE08 \uBC14\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694.`;
561
+ const localized = Object.fromEntries(locales.map((l) => [l, concise]));
562
+ return { concise, detailed, marketing, localized };
563
+ }
564
+ function parseFirstApp(text) {
565
+ try {
566
+ const apps = JSON.parse(text);
567
+ if (Array.isArray(apps) && apps.length > 0) return apps[0];
568
+ } catch {
569
+ }
570
+ return null;
571
+ }
572
+ async function cmdNotes(argv) {
573
+ const args = parseArgs2(argv);
574
+ const cwd = process.cwd();
575
+ process.stdout.write(kleur3.bold("mimi-seed notes \u2014 \uB9B4\uB9AC\uC988 \uB178\uD2B8 \uC0DD\uC131\n\n"));
576
+ if (!isGitRepo(cwd)) {
577
+ process.stdout.write(kleur3.red("Git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2D9\uB2C8\uB2E4.\n"));
578
+ process.exit(1);
579
+ }
580
+ const latestTag = getLatestTag(cwd);
581
+ const fromRef = args.from ?? latestTag ?? void 0;
582
+ process.stdout.write(kleur3.dim(fromRef ? `\uBC94\uC704: ${fromRef} \u2192 ${args.to}
583
+ ` : `\uCD5C\uADFC ${args.limit}\uAC1C \uCEE4\uBC0B
584
+ `));
585
+ const commits = getGitLog(cwd, { from: fromRef, to: args.to, limit: args.limit });
586
+ if (commits.length === 0) {
587
+ process.stdout.write(kleur3.yellow("\uCEE4\uBC0B\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.\n"));
588
+ process.exit(0);
589
+ }
590
+ process.stdout.write(kleur3.dim(`\uCEE4\uBC0B ${commits.length}\uAC1C \uBD84\uC11D \uC911...
591
+
592
+ `));
593
+ let result;
594
+ if (process.env.ANTHROPIC_API_KEY) {
595
+ process.stdout.write("\u{1F916} Claude AI\uB85C \uC0DD\uC131 \uC911...\n");
596
+ try {
597
+ result = await generateWithClaude(formatCommitsForPrompt(commits), args.locales);
598
+ } catch (e) {
599
+ process.stdout.write(kleur3.yellow(`AI \uC0DD\uC131 \uC2E4\uD328, \uD15C\uD50C\uB9BF \uC0AC\uC6A9: ${e.message}
600
+ `));
601
+ result = generateTemplate(commits, args.locales);
602
+ }
603
+ } else {
604
+ process.stdout.write(kleur3.dim("ANTHROPIC_API_KEY \uC5C6\uC74C \u2014 \uC790\uB3D9 \uD3EC\uB9F7\uD305 \uC0AC\uC6A9\n") + kleur3.dim("AI \uC0DD\uC131 \uD65C\uC131\uD654: export ANTHROPIC_API_KEY=sk-ant-...\n\n"));
605
+ result = generateTemplate(commits, args.locales);
606
+ }
607
+ process.stdout.write(kleur3.bold("\u2500\u2500\u2500 \uAC04\uACB0\uD55C \uBC84\uC804 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
608
+ process.stdout.write(result.concise + "\n\n");
609
+ process.stdout.write(kleur3.bold("\u2500\u2500\u2500 \uC0C1\uC138 \uBC84\uC804 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
610
+ process.stdout.write(result.detailed + "\n\n");
611
+ process.stdout.write(kleur3.bold("\u2500\u2500\u2500 \uB9C8\uCF00\uD305 \uBC84\uC804 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
612
+ process.stdout.write(result.marketing + "\n\n");
613
+ if (Object.keys(result.localized).length > 0) {
614
+ process.stdout.write(kleur3.bold("\u2500\u2500\u2500 \uB2E4\uAD6D\uC5B4 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
615
+ for (const [locale, text] of Object.entries(result.localized)) {
616
+ process.stdout.write(kleur3.dim(`[${locale}]
617
+ `) + text + "\n\n");
618
+ }
619
+ }
620
+ const shouldPrompt = !args.apply && !args.noInteractive && process.stdout.isTTY;
621
+ if (!args.apply && !shouldPrompt) return;
622
+ const cfg = await getEffectiveConfig();
623
+ if (!cfg) {
624
+ process.stdout.write(kleur3.yellow("Mimi Seed \uACC4\uC815 \uC5F0\uACB0 \uD544\uC694. `mimi-seed init` \uC2E4\uD589.\n"));
625
+ return;
626
+ }
627
+ let selectedText = result.concise;
628
+ if (shouldPrompt) {
629
+ const choice = await promptUser("\uC801\uC6A9\uD560 \uBC84\uC804 [1=\uAC04\uACB0/2=\uC0C1\uC138/3=\uB9C8\uCF00\uD305/Enter=\uAC74\uB108\uB700]: ");
630
+ if (!choice) {
631
+ process.stdout.write(kleur3.dim("\uAC74\uB108\uB700.\n"));
632
+ return;
633
+ }
634
+ if (choice === "2") selectedText = result.detailed;
635
+ else if (choice === "3") selectedText = result.marketing;
636
+ }
637
+ const appsResult = await mcpCall(cfg.endpoint, cfg.token, "list_apps", {});
638
+ if (appsResult.isError) {
639
+ process.stdout.write(kleur3.red(`\uC571 \uBAA9\uB85D \uC870\uD68C \uC2E4\uD328: ${appsResult.text}
640
+ `));
641
+ return;
642
+ }
643
+ const app = parseFirstApp(appsResult.text);
644
+ if (!app) {
645
+ process.stdout.write(kleur3.yellow("\uB4F1\uB85D\uB41C \uC571\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.\n"));
646
+ return;
647
+ }
648
+ process.stdout.write("Play Store\uC5D0 \uC801\uC6A9 \uC911...\n");
649
+ for (const locale of args.locales) {
650
+ const r = await mcpCall(cfg.endpoint, cfg.token, "apply_release_notes", {
651
+ app_id: app.id,
652
+ platform: "android",
653
+ locale,
654
+ text: result.localized[locale] ?? selectedText
655
+ });
656
+ process.stdout.write(r.isError ? kleur3.red(`${locale} \uC801\uC6A9 \uC2E4\uD328: ${r.text}
657
+ `) : kleur3.green(`\u2713 ${locale} \uC801\uC6A9\uB428
658
+ `));
659
+ }
660
+ }
661
+
662
+ // src/index.ts
663
+ var DEFAULT_WEB_BASE = process.env.MIMI_SEED_WEB_BASE ?? "https://mimi-seed.pryzm.gg";
664
+ var DEFAULT_MCP_ENDPOINT = `${DEFAULT_WEB_BASE}/api/mcp`;
665
+ function log(msg) {
666
+ process.stdout.write(msg + "\n");
667
+ }
668
+ async function cmdInit() {
669
+ const cwd = process.cwd();
670
+ log(kleur4.bold("Mimi Seed CLI \u2014 init"));
671
+ log(kleur4.dim(cwd));
672
+ log("");
673
+ if (!await hasAnyProjectSignal(cwd)) {
674
+ log(kleur4.yellow("\u26A0 package.json / app.json / android / ios \uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \uADF8\uB798\uB3C4 \uC9C4\uD589\uD569\uB2C8\uB2E4."));
675
+ }
676
+ log("\u{1F50D} \uC571 \uAC10\uC9C0 \uC911...");
677
+ const hints = await detectHints(cwd);
678
+ if (hints.length === 0) {
679
+ log(kleur4.yellow("\uAC10\uC9C0\uB41C \uC571 \uC5C6\uC74C. \uC6F9\uC5D0\uC11C \uC218\uB3D9 \uB4F1\uB85D \uAC00\uB2A5."));
680
+ } else {
681
+ for (const h of hints) {
682
+ const tag = [h.packageName && `android:${h.packageName}`, h.bundleId && `ios:${h.bundleId}`].filter(Boolean).join(" ");
683
+ log(` \u2022 ${h.name ?? "(\uC774\uB984 \uBBF8\uC0C1)"} ${kleur4.dim(tag)}`);
684
+ }
685
+ }
686
+ log("");
687
+ if (process.env.MIMI_SEED_TOKEN) {
688
+ const cfg2 = await getEffectiveConfig();
689
+ if (cfg2 && hints.length > 0) {
690
+ log(kleur4.dim("CI \uBAA8\uB4DC: MIMI_SEED_TOKEN \uC0AC\uC6A9"));
691
+ const payload = hints.map((h) => ({ name: h.name, packageName: h.packageName, bundleId: h.bundleId }));
692
+ const result = await mcpCall(cfg2.endpoint, cfg2.token, "sync_apps", { hints: payload });
693
+ if (result.isError) {
694
+ log(kleur4.red("\uB4F1\uB85D \uC2E4\uD328: " + result.text));
695
+ } else {
696
+ for (const line of result.text.split("\n")) log(" " + line);
697
+ }
698
+ }
699
+ log(kleur4.bold("\u2713 \uC644\uB8CC (CI \uBAA8\uB4DC)"));
700
+ return;
701
+ }
702
+ log("\u{1F510} \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uB85C\uADF8\uC778 \uB300\uAE30...");
703
+ const hostName = os2.hostname().slice(0, 32);
704
+ const name = `cli-${hostName}`;
705
+ const { port, promise } = await awaitHandshake(5 * 60 * 1e3);
706
+ const callback = `http://127.0.0.1:${port}/cb`;
707
+ const connectUrl = `${DEFAULT_WEB_BASE}/cli/connect?callback=${encodeURIComponent(callback)}&name=${encodeURIComponent(name)}`;
708
+ log(kleur4.dim(` ${connectUrl}`));
709
+ await open(connectUrl);
710
+ let handshake;
711
+ try {
712
+ handshake = await promise;
713
+ } catch (e) {
714
+ log(kleur4.red("\uC5F0\uACB0 \uC2E4\uD328: " + e.message));
715
+ process.exit(1);
716
+ }
717
+ log(kleur4.green("\u2713 \uD1A0\uD070 \uC218\uC2E0"));
718
+ const cfg = {
719
+ token: handshake.token,
720
+ prefix: handshake.prefix,
721
+ endpoint: DEFAULT_MCP_ENDPOINT,
722
+ webBase: DEFAULT_WEB_BASE,
723
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
724
+ };
725
+ await writeConfig(cfg);
726
+ log(kleur4.dim(` \uC800\uC7A5\uB428: ${CONFIG_LOCATION}`));
727
+ log("");
728
+ if (hints.length > 0) {
729
+ log("\u{1F504} \uC571 \uB4F1\uB85D \uC911...");
730
+ const payload = hints.map((h) => ({ name: h.name, packageName: h.packageName, bundleId: h.bundleId }));
731
+ const result = await mcpCall(cfg.endpoint, cfg.token, "sync_apps", { hints: payload });
732
+ if (result.isError) {
733
+ log(kleur4.red("\uB4F1\uB85D \uC2E4\uD328: " + result.text));
734
+ } else {
735
+ for (const line of result.text.split("\n")) log(" " + line);
736
+ }
737
+ log("");
738
+ }
739
+ log(kleur4.bold("\u2713 \uC900\uBE44 \uC644\uB8CC."));
740
+ log("");
741
+ log("Claude Code\uC5D0\uC11C \uC774\uB807\uAC8C \uBB3C\uC5B4\uBCF4\uC138\uC694:");
742
+ log(kleur4.cyan(' "\uB0B4 \uC571 \uCD9C\uC2DC \uC900\uBE44\uB410\uC5B4?"'));
743
+ log(kleur4.cyan(' "\uB9B4\uB9AC\uC988 \uB178\uD2B8 \uC368\uC918"'));
744
+ log(kleur4.cyan(' "\uB4F1\uB85D\uB41C \uC571 \uBAA9\uB85D \uBCF4\uC5EC\uC918"'));
745
+ log("");
746
+ log(`\uB300\uC2DC\uBCF4\uB4DC: ${kleur4.underline(DEFAULT_WEB_BASE + "/apps")}`);
747
+ log("");
748
+ log(
749
+ kleur4.dim(
750
+ `Claude Code MCP \uB4F1\uB85D:
751
+ claude mcp add --transport http mimi-seed ${DEFAULT_MCP_ENDPOINT} \\
752
+ --header "Authorization: Bearer ${cfg.prefix}..."`
753
+ )
754
+ );
755
+ }
756
+ async function cmdStatus() {
757
+ const cfg = await getEffectiveConfig();
758
+ if (!cfg) {
759
+ log(kleur4.yellow("\uC5F0\uACB0\uB41C Mimi Seed \uACC4\uC815\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. `mimi-seed init` \uC2E4\uD589."));
760
+ process.exit(1);
761
+ }
762
+ log(kleur4.bold("Mimi Seed \uC5F0\uACB0 \uC0C1\uD0DC"));
763
+ log(` \uD1A0\uD070: ${cfg.prefix}\u2026 (${cfg.createdAt.slice(0, 10)})`);
764
+ log(` \uC5D4\uB4DC\uD3EC\uC778\uD2B8: ${cfg.endpoint}`);
765
+ log("");
766
+ log("\u{1F4CB} \uC571 \uBAA9\uB85D:");
767
+ const r = await mcpCall(cfg.endpoint, cfg.token, "list_apps", {});
768
+ if (r.isError) {
769
+ log(kleur4.red("\uC870\uD68C \uC2E4\uD328: " + r.text));
770
+ process.exit(1);
771
+ }
772
+ for (const line of r.text.split("\n")) log(" " + line);
773
+ }
774
+ async function cmdLogout() {
775
+ await deleteConfig();
776
+ log(kleur4.green("\u2713 \uB85C\uCEEC \uC124\uC815 \uC0AD\uC81C \uC644\uB8CC."));
777
+ log(kleur4.dim("\uC6F9\uC5D0\uC11C \uD1A0\uD070 \uD574\uC9C0: /workspace/api-tokens"));
778
+ }
779
+ function printHelp() {
780
+ log(`${kleur4.bold("mimi-seed")} \u2014 Claude Code\uC5D0\uC11C \uC571 \uCD9C\uC2DC \uC6B4\uC601
781
+
782
+ ${kleur4.bold("\uBA85\uB839\uC5B4:")}
783
+ ${kleur4.cyan("mimi-seed init")} \uD604\uC7AC \uD504\uB85C\uC81D\uD2B8\uB97C Mimi Seed\uC5D0 \uC5F0\uACB0
784
+ ${kleur4.cyan("mimi-seed status")} \uC5F0\uACB0 \uC0C1\uD0DC + \uB4F1\uB85D \uC571 \uBAA9\uB85D
785
+ ${kleur4.cyan("mimi-seed doctor")} \uD658\uACBD \uC9C4\uB2E8 (\uD1A0\uD070\xB7Git\xB7\uD504\uB85C\uC81D\uD2B8\xB7CI \uCCB4\uD06C)
786
+ ${kleur4.cyan("mimi-seed check")} \uCD9C\uC2DC \uC804 Readiness \uC810\uAC80
787
+ ${kleur4.cyan("mimi-seed notes")} \uB9B4\uB9AC\uC988 \uB178\uD2B8 \uC0DD\uC131 (git log \u2192 AI \u2192 \uB9C8\uCF13 \uC801\uC6A9)
788
+ ${kleur4.cyan("mimi-seed logout")} \uB85C\uCEEC \uC124\uC815 \uC0AD\uC81C
789
+
790
+ ${kleur4.bold("mimi-seed notes \uC635\uC158:")}
791
+ --from <ref> \uC2DC\uC791 \uCEE4\uBC0B/\uD0DC\uADF8 (\uAE30\uBCF8: \uCD5C\uC2E0 \uD0DC\uADF8)
792
+ --to <ref> \uB05D \uCEE4\uBC0B (\uAE30\uBCF8: HEAD)
793
+ --locale ko,en-US \uB300\uC0C1 \uB85C\uCF00\uC77C (\uC27C\uD45C \uAD6C\uBD84)
794
+ --apply \uC0DD\uC131 \uD6C4 \uC2A4\uD1A0\uC5B4\uC5D0 \uBC14\uB85C \uC801\uC6A9
795
+ --no-interactive CI \uBAA8\uB4DC (\uD504\uB86C\uD504\uD2B8 \uC5C6\uC74C)
796
+ --limit <n> \uCD5C\uB300 \uCEE4\uBC0B \uC218 (\uAE30\uBCF8: 30)
797
+
798
+ ${kleur4.bold("mimi-seed check \uC635\uC158:")}
799
+ --app <id> \uC571 ID \uC9C0\uC815
800
+ --fail-on-blocker \uBE14\uB85C\uCEE4 \uC788\uC73C\uBA74 exit 1 (CI\uC6A9)
801
+
802
+ ${kleur4.bold("\uD658\uACBD\uBCC0\uC218:")}
803
+ MIMI_SEED_TOKEN PAT \uD1A0\uD070 (CI/CD \uBB34\uC778\uC99D \uBAA8\uB4DC)
804
+ MIMI_SEED_WEB_BASE \uC11C\uBC84 \uC8FC\uC18C (\uAE30\uBCF8: https://mimi-seed.pryzm.gg)
805
+ ANTHROPIC_API_KEY AI \uB178\uD2B8 \uC0DD\uC131 \uD65C\uC131\uD654 (\uC120\uD0DD)
806
+ `);
807
+ }
808
+ async function main() {
809
+ const cmd = process.argv[2];
810
+ const restArgs = process.argv.slice(3);
811
+ try {
812
+ switch (cmd) {
813
+ case "init":
814
+ await cmdInit();
815
+ break;
816
+ case "status":
817
+ await cmdStatus();
818
+ break;
819
+ case "doctor":
820
+ await cmdDoctor();
821
+ break;
822
+ case "check":
823
+ await cmdCheck(restArgs);
824
+ break;
825
+ case "notes":
826
+ await cmdNotes(restArgs);
827
+ break;
828
+ case "logout":
829
+ await cmdLogout();
830
+ break;
831
+ case "--help":
832
+ case "-h":
833
+ case void 0:
834
+ printHelp();
835
+ break;
836
+ default:
837
+ log(kleur4.red(`\uC54C \uC218 \uC5C6\uB294 \uBA85\uB839: ${cmd}`));
838
+ printHelp();
839
+ process.exit(1);
840
+ }
841
+ } catch (e) {
842
+ log(kleur4.red(`\uC624\uB958: ${e.message}`));
843
+ if (process.env.DEBUG) log(e.stack ?? "");
844
+ process.exit(1);
845
+ }
846
+ }
847
+ void main();
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "mimi-seed",
3
+ "version": "0.2.0",
4
+ "description": "Mimi Seed CLI — Claude Code에서 앱 출시 운영을 관리합니다.",
5
+ "bin": {
6
+ "mimi-seed": "./dist/index.js"
7
+ },
8
+ "type": "module",
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsup",
14
+ "dev": "tsx src/index.ts"
15
+ },
16
+ "engines": {
17
+ "node": ">=18"
18
+ },
19
+ "dependencies": {
20
+ "@anthropic-ai/sdk": "^0.52.0",
21
+ "kleur": "^4.1.5",
22
+ "open": "^10.1.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^20.11.0",
26
+ "tsup": "^8.0.0",
27
+ "tsx": "^4.7.0",
28
+ "typescript": "^5.4.0"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ }
33
+ }