@yoonion/mimi-seed-mcp 0.6.6 → 0.8.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.
package/README.md CHANGED
@@ -42,17 +42,29 @@ npx -y @yoonion/mimi-seed-mcp mimi-seed-auth
42
42
 
43
43
  브라우저가 열리면 Google 계정으로 로그인. 토큰은 `~/.mimi-seed/tokens.json`에 저장되고 자동 갱신됨.
44
44
 
45
- 플랫폼별 추가 인증 (필요한 것만):
45
+ 나머지 계정은 **마법사 하나로** 연결하는 게 가장 빠릅니다 — 뭐가 빠졌는지 보여주고, 각 토큰을 어디서
46
+ 발급받는지도 알려줍니다:
46
47
 
47
48
  ```bash
48
- npx -y @yoonion/mimi-seed-mcp mimi-seed-appstore-auth # App Store Connect (API Key)
49
- npx -y @yoonion/mimi-seed-mcp mimi-seed-playstore-auth # Google Play 서비스 계정 JSON
50
- npx -y @yoonion/mimi-seed-mcp mimi-seed-bigquery-auth # BigQuery (Crashlytics export 등)
49
+ npx mimi-seed setup
51
50
  ```
52
51
 
53
- - **App Store Connect**: Users and Access → Keys에서 API Key 생성 후 Issuer ID / Key ID / .p8 경로 입력 → `~/.mimi-seed/appstore.json`
54
- - **Google Play**: Play Console 서비스 계정 JSON 경로 입력 (패키지별 등록)
55
- - **BigQuery**: Google OAuth 또는 서비스 계정으로 데이터셋 조회 권한 부여
52
+ 개별로 실행하고 싶다면 (전부 대화형):
53
+
54
+ ```bash
55
+ npx -y @yoonion/mimi-seed-mcp mimi-seed-appstore-auth # App Store Connect (API Key)
56
+ npx -y @yoonion/mimi-seed-mcp mimi-seed-playstore-auth # Google Play 서비스 계정 JSON
57
+ npx -y @yoonion/mimi-seed-mcp mimi-seed-bigquery-auth # BigQuery
58
+ npx -y @yoonion/mimi-seed-mcp mimi-seed-jenkins-auth # Jenkins (저장 전 서버 프로브)
59
+ npx -y @yoonion/mimi-seed-mcp mimi-seed-googleads-auth # Google Ads (저장 전 실제 호출로 검증)
60
+ npx -y @yoonion/mimi-seed-mcp mimi-seed-social-auth # Facebook / Instagram
61
+ ```
62
+
63
+ 각 자격증명을 **어디서 어떻게 발급받는지**는 [`docs/credentials.md`](../../docs/credentials.md) 참고.
64
+
65
+ - **App Store Connect**: Users and Access → Integrations에서 API Key 생성 후 Issuer ID / Key ID / .p8 경로 입력 → `~/.mimi-seed/appstore.json` (.p8은 **1회만** 다운로드됨)
66
+ - **Google Play**: 서비스 계정은 **선택** — OAuth 토큰이 `androidpublisher` 스코프를 갖고 있어 로컬 작업은 그대로 됩니다. CI/헤드리스에서만 필요.
67
+ - **BigQuery**: 선택 — OAuth로도 동작하며, Workspace 재인증 정책에 막힐 때만 서비스 계정이 필요합니다.
56
68
 
57
69
  AI 기능(릴리즈 노트 생성, 리뷰 답변)을 쓰려면:
58
70
 
@@ -66,8 +78,8 @@ export ANTHROPIC_API_KEY=sk-ant-...
66
78
 
67
79
  | 영역 | 도구 수 | 주요 도구 |
68
80
  |------|---------|-----------|
69
- | App Store Connect | 33 | `appstore_submit_for_review` / `appstore_upload_screenshot` / `appstore_update_product_review_note` / `appstore_upload_product_review_screenshot` |
70
- | Google Play | 28 | `playstore_submit_release` / `playstore_promote_release` / `playstore_replace_images` / `playstore_reply_review` / `playstore_verify_service_account` |
81
+ | App Store Connect | 34 | `appstore_submit_for_review` / `appstore_upload_screenshot` / `appstore_update_product_review_note` / `appstore_upload_product_review_screenshot` |
82
+ | Google Play | 29 | `playstore_submit_release` / `playstore_promote_release` / `playstore_replace_images` / `playstore_reply_review` / `playstore_verify_service_account` |
71
83
  | Firebase | 20 | `firebase_create_project` / `firebase_create_android_app` / `firebase_get_android_config` / `firebase_create_ios_app` |
72
84
  | AdMob | 7 | `admob_list_apps` / `admob_create_ad_unit` / `admob_get_today_earnings` / `admob_get_report` |
73
85
  | CI/CD (GitHub Actions · GitLab) | 6 | `ci_trigger_build` / `ci_get_build_status` / `ci_list_workflows` / `ci_cancel_build` |
@@ -2,34 +2,71 @@
2
2
  import { saveAppStoreCredentials, getAppStoreCredentials } from './auth.js';
3
3
  import readline from 'node:readline';
4
4
  import fs from 'node:fs';
5
+ import { resolveLang } from '../lib/lang.js';
6
+ // ko 가 원본이고 en 은 `typeof ko` 를 만족해야 한다 — 키를 빠뜨리면 컴파일이 깨진다.
7
+ const ko = {
8
+ title: ' 🍎 Mimi Seed — App Store Connect 연결',
9
+ already: (keyId) => ` ✅ 이미 연결됨 (Key ID: ${keyId})`,
10
+ reconnect: ' 다시 설정할래? (y/N): ',
11
+ needTitle: ' App Store Connect에서 API Key를 만들어야 해:',
12
+ step1: ' 1. "키 생성" 클릭',
13
+ step2: ' 2. 이름: Mimi Seed, 역할: Admin',
14
+ step3: ' 3. .p8 파일 다운로드 (1회만 가능!)',
15
+ askIssuerId: ' Issuer ID: ',
16
+ askKeyId: ' Key ID: ',
17
+ askP8Path: ' .p8 파일 경로: ',
18
+ noFile: (p) => ` ❌ 파일 없음: ${p}`,
19
+ done: ' ✅ 연결 완료!',
20
+ nextTitle: ' 이제 Claude Code 또는 Codex에서:',
21
+ nextExample1: ' "내 앱스토어 앱 목록 보여줘"',
22
+ nextExample2: ' "TestFlight 빌드 목록 보여줘"',
23
+ };
24
+ const en = {
25
+ title: ' 🍎 Mimi Seed — Connect App Store Connect',
26
+ already: (keyId) => ` ✅ Already connected (Key ID: ${keyId})`,
27
+ reconnect: ' Set it up again? (y/N): ',
28
+ needTitle: ' You need to create an API Key in App Store Connect:',
29
+ step1: ' 1. Click "Generate API Key"',
30
+ step2: ' 2. Name: Mimi Seed, Access: Admin',
31
+ step3: ' 3. Download the .p8 file (you can only download it ONCE!)',
32
+ askIssuerId: ' Issuer ID: ',
33
+ askKeyId: ' Key ID: ',
34
+ askP8Path: ' Path to the .p8 file: ',
35
+ noFile: (p) => ` ❌ File not found: ${p}`,
36
+ done: ' ✅ Connected!',
37
+ nextTitle: ' Now, in Claude Code or Codex:',
38
+ nextExample1: ' "Show my App Store apps"',
39
+ nextExample2: ' "List the TestFlight builds"',
40
+ };
41
+ const M = resolveLang() === 'en' ? en : ko;
5
42
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
6
43
  const ask = (q) => new Promise((r) => rl.question(q, r));
7
44
  async function main() {
8
45
  console.log('');
9
- console.log(' 🍎 Mimi Seed — App Store Connect 연결');
46
+ console.log(M.title);
10
47
  console.log('');
11
48
  const existing = getAppStoreCredentials();
12
49
  if (existing) {
13
- console.log(` ✅ 이미 연결됨 (Key ID: ${existing.keyId})`);
14
- const answer = await ask(' 다시 설정할래? (y/N): ');
50
+ console.log(M.already(existing.keyId));
51
+ const answer = await ask(M.reconnect);
15
52
  if (answer.toLowerCase() !== 'y') {
16
53
  rl.close();
17
54
  return;
18
55
  }
19
56
  }
20
- console.log(' App Store Connect에서 API Key를 만들어야 해:');
57
+ console.log(M.needTitle);
21
58
  console.log(' https://appstoreconnect.apple.com/access/integrations/api');
22
59
  console.log('');
23
- console.log(' 1. "키 생성" 클릭');
24
- console.log(' 2. 이름: Mimi Seed, 역할: Admin');
25
- console.log(' 3. .p8 파일 다운로드 (1회만 가능!)');
60
+ console.log(M.step1);
61
+ console.log(M.step2);
62
+ console.log(M.step3);
26
63
  console.log('');
27
- const issuerId = await ask(' Issuer ID: ');
28
- const keyId = await ask(' Key ID: ');
29
- const p8Path = await ask(' .p8 파일 경로: ');
64
+ const issuerId = await ask(M.askIssuerId);
65
+ const keyId = await ask(M.askKeyId);
66
+ const p8Path = await ask(M.askP8Path);
30
67
  const trimmedPath = p8Path.trim().replace(/^["']|["']$/g, '');
31
68
  if (!fs.existsSync(trimmedPath)) {
32
- console.log(` ❌ 파일 없음: ${trimmedPath}`);
69
+ console.log(M.noFile(trimmedPath));
33
70
  rl.close();
34
71
  process.exit(1);
35
72
  }
@@ -40,11 +77,11 @@ async function main() {
40
77
  privateKey,
41
78
  });
42
79
  console.log('');
43
- console.log(' ✅ 연결 완료!');
80
+ console.log(M.done);
44
81
  console.log('');
45
- console.log(' 이제 Claude Code 또는 Codex에서:');
46
- console.log(' "내 앱스토어 앱 목록 보여줘"');
47
- console.log(' "TestFlight 빌드 목록 보여줘"');
82
+ console.log(M.nextTitle);
83
+ console.log(M.nextExample1);
84
+ console.log(M.nextExample2);
48
85
  console.log('');
49
86
  rl.close();
50
87
  }
@@ -4,36 +4,89 @@ import fs from 'node:fs';
4
4
  import { saveBigQueryServiceAccountJson, getBigQueryServiceAccountKey, } from './bigquery-auth.js';
5
5
  import * as bigquery from '../bigquery/tools.js';
6
6
  import { JWT } from 'google-auth-library';
7
+ import { resolveLang } from '../lib/lang.js';
8
+ // ko 가 원본이고 en 은 `typeof ko` 를 만족해야 한다 — 키를 빠뜨리면 컴파일이 깨진다.
9
+ const ko = {
10
+ title: ' 🤖 Mimi Seed — BigQuery 서비스 계정 연결',
11
+ whySa1: ' 서비스 계정 인증은 Google Workspace 의 재인증(reauth) 정책에서',
12
+ whySa2: ' 면제되므로, OAuth 가 invalid_rapt 로 막히는 환경에서도 동작해.',
13
+ already: (email) => ` ✅ 이미 연결됨 (${email})`,
14
+ reconnect: ' 다시 설정할래? (y/N): ',
15
+ needTitle: ' BigQuery 를 읽을 수 있는 서비스 계정 키 JSON 이 필요해:',
16
+ step1: ' 1. GCP Console → IAM & Admin → Service Accounts',
17
+ step2: ' 2. 서비스 계정 선택(또는 생성) → Keys → Add Key → JSON',
18
+ step3: ' 3. 그 서비스 계정에 프로젝트 IAM 역할 부여:',
19
+ roleJobUser: ' • roles/bigquery.jobUser (쿼리 작업 생성)',
20
+ roleDataViewer: ' • roles/bigquery.dataViewer (데이터셋 읽기)',
21
+ step4: ' 4. 다운로드한 JSON 파일 경로를 입력해',
22
+ askPath: ' 서비스 계정 JSON 파일 경로: ',
23
+ noFile: (p) => ` ❌ 파일 없음: ${p}`,
24
+ saved: (email) => ` ✅ 저장 완료! (${email})`,
25
+ saProject: (p) => ` 서비스 계정 프로젝트: ${p}`,
26
+ askTestProject: (fallback) => ` 연결 테스트할 GCP 프로젝트 ID (엔터 시 건너뜀${fallback ? `, 기본 ${fallback}` : ''}): `,
27
+ probing: (projectId) => ` 🔎 ${projectId} 데이터셋 조회 중...`,
28
+ probeOk: (n) => ` ✅ 접근 OK — 데이터셋 ${n}개`,
29
+ probeFail: (msg) => ` ⚠️ 접근 실패: ${msg}`,
30
+ grantHint: ' IAM 역할이 없으면 아래를 실행해 (프로젝트 소유자 계정에서):',
31
+ nextTitle: ' 이제 Claude Code 또는 Codex에서:',
32
+ nextExample: ' "BigQuery 로 GA4 트래픽 분석해줘"',
33
+ };
34
+ const en = {
35
+ title: ' 🤖 Mimi Seed — Connect a BigQuery service account',
36
+ whySa1: ' Service-account auth is exempt from the Google Workspace reauth policy,',
37
+ whySa2: ' so it still works in environments where OAuth is blocked by invalid_rapt.',
38
+ already: (email) => ` ✅ Already connected (${email})`,
39
+ reconnect: ' Set it up again? (y/N): ',
40
+ needTitle: ' A service-account key JSON that can read BigQuery is required:',
41
+ step1: ' 1. GCP Console → IAM & Admin → Service Accounts',
42
+ step2: ' 2. Pick (or create) a service account → Keys → Add Key → JSON',
43
+ step3: ' 3. Grant that service account these project IAM roles yourself:',
44
+ roleJobUser: ' • roles/bigquery.jobUser (create query jobs)',
45
+ roleDataViewer: ' • roles/bigquery.dataViewer (read datasets)',
46
+ step4: ' 4. Enter the path to the downloaded JSON file',
47
+ askPath: ' Path to the service-account JSON file: ',
48
+ noFile: (p) => ` ❌ File not found: ${p}`,
49
+ saved: (email) => ` ✅ Saved! (${email})`,
50
+ saProject: (p) => ` Service-account project: ${p}`,
51
+ askTestProject: (fallback) => ` GCP project ID to test against (Enter to skip${fallback ? `, default ${fallback}` : ''}): `,
52
+ probing: (projectId) => ` 🔎 Listing datasets in ${projectId}...`,
53
+ probeOk: (n) => ` ✅ Access OK — ${n} dataset(s)`,
54
+ probeFail: (msg) => ` ⚠️ Access failed: ${msg}`,
55
+ grantHint: ' If the IAM roles are missing, run this (as a project owner):',
56
+ nextTitle: ' Now, in Claude Code or Codex:',
57
+ nextExample: ' "Analyze GA4 traffic with BigQuery"',
58
+ };
59
+ const M = resolveLang() === 'en' ? en : ko;
7
60
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
8
61
  const ask = (q) => new Promise((r) => rl.question(q, r));
9
62
  async function main() {
10
63
  console.log('');
11
- console.log(' 🤖 Mimi Seed — BigQuery 서비스 계정 연결');
64
+ console.log(M.title);
12
65
  console.log('');
13
- console.log(' 서비스 계정 인증은 Google Workspace 의 재인증(reauth) 정책에서');
14
- console.log(' 면제되므로, OAuth 가 invalid_rapt 로 막히는 환경에서도 동작해.');
66
+ console.log(M.whySa1);
67
+ console.log(M.whySa2);
15
68
  console.log('');
16
69
  const existing = getBigQueryServiceAccountKey();
17
70
  if (existing) {
18
- console.log(` ✅ 이미 연결됨 (${existing.client_email})`);
19
- const answer = await ask(' 다시 설정할래? (y/N): ');
71
+ console.log(M.already(existing.client_email));
72
+ const answer = await ask(M.reconnect);
20
73
  if (answer.toLowerCase() !== 'y') {
21
74
  rl.close();
22
75
  return;
23
76
  }
24
77
  }
25
- console.log(' BigQuery 를 읽을 수 있는 서비스 계정 키 JSON 이 필요해:');
26
- console.log(' 1. GCP Console → IAM & Admin → Service Accounts');
27
- console.log(' 2. 서비스 계정 선택(또는 생성) → Keys → Add Key → JSON');
28
- console.log(' 3. 그 서비스 계정에 프로젝트 IAM 역할 부여:');
29
- console.log(' • roles/bigquery.jobUser (쿼리 작업 생성)');
30
- console.log(' • roles/bigquery.dataViewer (데이터셋 읽기)');
31
- console.log(' 4. 다운로드한 JSON 파일 경로를 입력해');
78
+ console.log(M.needTitle);
79
+ console.log(M.step1);
80
+ console.log(M.step2);
81
+ console.log(M.step3);
82
+ console.log(M.roleJobUser);
83
+ console.log(M.roleDataViewer);
84
+ console.log(M.step4);
32
85
  console.log('');
33
- const jsonPath = await ask(' 서비스 계정 JSON 파일 경로: ');
86
+ const jsonPath = await ask(M.askPath);
34
87
  const trimmedPath = jsonPath.trim().replace(/^["']|["']$/g, '');
35
88
  if (!fs.existsSync(trimmedPath)) {
36
- console.log(` ❌ 파일 없음: ${trimmedPath}`);
89
+ console.log(M.noFile(trimmedPath));
37
90
  rl.close();
38
91
  process.exit(1);
39
92
  }
@@ -48,15 +101,15 @@ async function main() {
48
101
  process.exit(1);
49
102
  }
50
103
  console.log('');
51
- console.log(` ✅ 저장 완료! (${parsed.client_email})`);
104
+ console.log(M.saved(parsed.client_email));
52
105
  if (parsed.project_id)
53
- console.log(` 서비스 계정 프로젝트: ${parsed.project_id}`);
106
+ console.log(M.saProject(parsed.project_id));
54
107
  console.log('');
55
108
  // 선택적 연결 테스트 — projectId 를 입력하면 datasets.list 로 권한 확인.
56
- const testProject = await ask(` 연결 테스트할 GCP 프로젝트 ID (엔터 시 건너뜀${parsed.project_id ? `, 기본 ${parsed.project_id}` : ''}): `);
109
+ const testProject = await ask(M.askTestProject(parsed.project_id ?? ''));
57
110
  const projectId = testProject.trim() || parsed.project_id || '';
58
111
  if (projectId) {
59
- console.log(` 🔎 ${projectId} 데이터셋 조회 중...`);
112
+ console.log(M.probing(projectId));
60
113
  try {
61
114
  const jwt = new JWT({
62
115
  email: parsed.client_email,
@@ -67,15 +120,15 @@ async function main() {
67
120
  ],
68
121
  });
69
122
  const datasets = await bigquery.listDatasets(jwt, projectId);
70
- console.log(` ✅ 접근 OK — 데이터셋 ${datasets.length}개`);
123
+ console.log(M.probeOk(datasets.length));
71
124
  for (const d of datasets.slice(0, 10))
72
125
  console.log(` • ${d.datasetId} (${d.location})`);
73
126
  }
74
127
  catch (e) {
75
128
  const msg = e instanceof Error ? e.message : String(e);
76
- console.log(` ⚠️ 접근 실패: ${msg}`);
129
+ console.log(M.probeFail(msg));
77
130
  console.log('');
78
- console.log(' IAM 역할이 없으면 아래를 실행해 (프로젝트 소유자 계정에서):');
131
+ console.log(M.grantHint);
79
132
  console.log(` gcloud projects add-iam-policy-binding ${projectId} \\`);
80
133
  console.log(` --member="serviceAccount:${parsed.client_email}" --role="roles/bigquery.jobUser"`);
81
134
  console.log(` gcloud projects add-iam-policy-binding ${projectId} \\`);
@@ -83,8 +136,8 @@ async function main() {
83
136
  }
84
137
  }
85
138
  console.log('');
86
- console.log(' 이제 Claude Code 또는 Codex에서:');
87
- console.log(' "BigQuery 로 GA4 트래픽 분석해줘"');
139
+ console.log(M.nextTitle);
140
+ console.log(M.nextExample);
88
141
  console.log('');
89
142
  rl.close();
90
143
  }
package/dist/auth/cli.js CHANGED
@@ -4,20 +4,12 @@ import open from 'open';
4
4
  import { startAuth, getStoredTokens, ensureFreshAccessToken, } from './google-auth.js';
5
5
  import { AuthError, classifyError } from './errors.js';
6
6
  import { getMcpOAuthClient } from './constants.js';
7
- const args = process.argv.slice(2);
8
- const hasFlag = (name) => args.includes(`--${name}`);
9
- const flagValue = (name) => {
10
- const i = args.indexOf(`--${name}`);
11
- if (i < 0)
12
- return undefined;
13
- const v = args[i + 1];
14
- return v && !v.startsWith('--') ? v : undefined;
15
- };
16
- function err(msg) {
17
- process.stderr.write(msg + '\n');
18
- }
19
- function printHelp() {
20
- err(`
7
+ import { resolveLang } from '../lib/lang.js';
8
+ // ko 원본이고 en `typeof ko` 를 만족해야 한다 — 키를 빠뜨리면 컴파일이 깨진다.
9
+ // 여기 있는 전부 **터미널에 찍히는 사람용 문자열**이다. errors.ts 가 만드는
10
+ // code/message/hint 라이브러리 텍스트라 그대로 출력한다.
11
+ const ko = {
12
+ help: `
21
13
  ☕ mimi-seed-auth — Google OAuth 인증 CLI
22
14
 
23
15
  사용법:
@@ -31,21 +23,132 @@ function printHelp() {
31
23
  --timeout <초> 콜백 대기 시간 (기본 600)
32
24
  --force 기존 토큰 무시하고 강제 재로그인
33
25
  --help 이 도움말
34
- `);
26
+ `,
27
+ expired: '만료됨',
28
+ minsLeft: (n) => `${n}분 남음`,
29
+ hoursLeft: (n) => `${n}시간 남음`,
30
+ daysLeft: (n) => `${n}일 남음`,
31
+ errCode: (code) => ` 코드: ${code}`,
32
+ statusTitle: ' ☕ Mimi Seed — 인증 상태',
33
+ statusFresh: (left) => ` ✅ 연결됨 — 토큰 유효 (${left})`,
34
+ statusRefreshed: (left) => ` ✅ 연결됨 — refresh_token으로 갱신 (${left})`,
35
+ statusExpired: ' ⚠️ 토큰 만료 + 자동 갱신 실패',
36
+ statusNone: ' ❌ 연결된 계정 없음.',
37
+ refreshTrying: ' 🔄 refresh_token으로 갱신 시도 중...',
38
+ refreshNotNeeded: (left) => ` ✅ 토큰 유효 — 갱신 불필요 (${left})`,
39
+ refreshDone: (left) => ` ✅ 갱신 완료 (${left})`,
40
+ refreshFailed: ' ❌ refresh 실패',
41
+ refreshNoToken: ' ❌ 저장된 토큰 없음.',
42
+ logoutDone: ' ✅ 토큰 삭제 완료.',
43
+ logoutAlready: ' (이미 삭제된 상태)',
44
+ loginTitle: ' ☕ Mimi Seed — Google 계정 연결',
45
+ loginChecking: ' 🔍 기존 토큰 검사 중...',
46
+ loginValid: '유효함',
47
+ loginRefreshed: 'refresh_token으로 갱신 완료',
48
+ loginAlready: (label, left) => ` ✅ 이미 연결됨 (${label}, ${left}).`,
49
+ loginAgain: ' 다시 로그인할래? (y/N): ',
50
+ loginExpiredRelogin: (code) => ` ⚠️ 토큰 만료 + 자동 갱신 실패 [${code}] — 재로그인 진행.`,
51
+ serverStart: ' 🌐 OAuth 콜백 서버 시작: http://localhost:9876/callback',
52
+ serverFail: ' ❌ 콜백 서버 시작 실패',
53
+ pasteUrl: ' 📋 아래 URL을 브라우저에 직접 붙여넣으세요:',
54
+ openingBrowser: ' 🌐 기본 브라우저 자동 열기...',
55
+ openingHint: ' (실패 시 --no-browser 로 URL 직접 받기)',
56
+ openFail: (msg) => ` ⚠️ 브라우저 자동 열기 실패: ${msg}`,
57
+ openManually: ' 📋 직접 열어주세요:',
58
+ waiting: (sec) => ` ⏳ Google 승인 대기 중... (timeout ${sec}s)`,
59
+ authFailed: ' ❌ 인증 실패',
60
+ done: ' ✅ 연결 완료!',
61
+ nextTitle: ' 이제 Claude Code 또는 Codex에서 이렇게 쓸 수 있어:',
62
+ nextExample1: ' "내 Firebase 프로젝트 보여줘"',
63
+ nextExample2: ' "새 Android 앱 등록해줘"',
64
+ nextExample3: ' "google-services.json 다운로드해줘"',
65
+ fatal: (msg) => ` ❌ 예외: ${msg}`,
66
+ };
67
+ const en = {
68
+ help: `
69
+ ☕ mimi-seed-auth — Google OAuth CLI
70
+
71
+ Usage:
72
+ mimi-seed-auth # log in (tries a silent refresh if a token exists)
73
+ mimi-seed-auth --refresh # only refresh with refresh_token (no browser)
74
+ mimi-seed-auth --status # print the current token status
75
+ mimi-seed-auth --logout # delete the token
76
+
77
+ Options:
78
+ --no-browser Do not open the URL automatically (copy-paste it yourself)
79
+ --timeout <sec> How long to wait for the callback (default 600)
80
+ --force Ignore the existing token and force a re-login
81
+ --help This help
82
+ `,
83
+ expired: 'expired',
84
+ minsLeft: (n) => `${n} min left`,
85
+ hoursLeft: (n) => `${n} hr left`,
86
+ daysLeft: (n) => `${n} days left`,
87
+ errCode: (code) => ` code: ${code}`,
88
+ statusTitle: ' ☕ Mimi Seed — Auth status',
89
+ statusFresh: (left) => ` ✅ Connected — token valid (${left})`,
90
+ statusRefreshed: (left) => ` ✅ Connected — refreshed with refresh_token (${left})`,
91
+ statusExpired: ' ⚠️ Token expired + automatic refresh failed',
92
+ statusNone: ' ❌ No connected account.',
93
+ refreshTrying: ' 🔄 Trying to refresh with refresh_token...',
94
+ refreshNotNeeded: (left) => ` ✅ Token still valid — no refresh needed (${left})`,
95
+ refreshDone: (left) => ` ✅ Refreshed (${left})`,
96
+ refreshFailed: ' ❌ Refresh failed',
97
+ refreshNoToken: ' ❌ No stored token.',
98
+ logoutDone: ' ✅ Token deleted.',
99
+ logoutAlready: ' (already deleted)',
100
+ loginTitle: ' ☕ Mimi Seed — Connect your Google account',
101
+ loginChecking: ' 🔍 Checking the existing token...',
102
+ loginValid: 'valid',
103
+ loginRefreshed: 'refreshed with refresh_token',
104
+ loginAlready: (label, left) => ` ✅ Already connected (${label}, ${left}).`,
105
+ loginAgain: ' Log in again? (y/N): ',
106
+ loginExpiredRelogin: (code) => ` ⚠️ Token expired + automatic refresh failed [${code}] — re-logging in.`,
107
+ serverStart: ' 🌐 Starting the OAuth callback server: http://localhost:9876/callback',
108
+ serverFail: ' ❌ Failed to start the callback server',
109
+ pasteUrl: ' 📋 Paste this URL into your browser:',
110
+ openingBrowser: ' 🌐 Opening your default browser...',
111
+ openingHint: ' (if that fails, use --no-browser to get the URL)',
112
+ openFail: (msg) => ` ⚠️ Could not open the browser: ${msg}`,
113
+ openManually: ' 📋 Please open it yourself:',
114
+ waiting: (sec) => ` ⏳ Waiting for Google approval... (timeout ${sec}s)`,
115
+ authFailed: ' ❌ Authentication failed',
116
+ done: ' ✅ Connected!',
117
+ nextTitle: ' Now you can say things like this in Claude Code or Codex:',
118
+ nextExample1: ' "Show my Firebase projects"',
119
+ nextExample2: ' "Register a new Android app"',
120
+ nextExample3: ' "Download google-services.json"',
121
+ fatal: (msg) => ` ❌ Exception: ${msg}`,
122
+ };
123
+ const M = resolveLang() === 'en' ? en : ko;
124
+ const args = process.argv.slice(2);
125
+ const hasFlag = (name) => args.includes(`--${name}`);
126
+ const flagValue = (name) => {
127
+ const i = args.indexOf(`--${name}`);
128
+ if (i < 0)
129
+ return undefined;
130
+ const v = args[i + 1];
131
+ return v && !v.startsWith('--') ? v : undefined;
132
+ };
133
+ function err(msg) {
134
+ process.stderr.write(msg + '\n');
135
+ }
136
+ function printHelp() {
137
+ err(M.help);
35
138
  }
36
139
  function fmtRemaining(ms) {
37
140
  if (ms <= 0)
38
- return '만료됨';
141
+ return M.expired;
39
142
  const min = Math.round(ms / 60000);
40
143
  if (min < 60)
41
- return `${min}분 남음`;
144
+ return M.minsLeft(min);
42
145
  const hr = Math.round(min / 60);
43
146
  if (hr < 48)
44
- return `${hr}시간 남음`;
45
- return `${Math.round(hr / 24)}일 남음`;
147
+ return M.hoursLeft(hr);
148
+ return M.daysLeft(Math.round(hr / 24));
46
149
  }
47
150
  function printAuthError(p) {
48
- err(` 코드: ${p.code}`);
151
+ err(M.errCode(p.code));
49
152
  err(` ${p.message}`);
50
153
  if (p.hint)
51
154
  err(` → ${p.hint}`);
@@ -54,25 +157,25 @@ function printAuthError(p) {
54
157
  }
55
158
  async function cmdStatus() {
56
159
  err('');
57
- err(' ☕ Mimi Seed — 인증 상태');
160
+ err(M.statusTitle);
58
161
  err('');
59
162
  const r = await ensureFreshAccessToken();
60
163
  switch (r.status) {
61
164
  case 'fresh':
62
- err(` ✅ 연결됨 — 토큰 유효 (${fmtRemaining(r.msUntilExpiry)})`);
165
+ err(M.statusFresh(fmtRemaining(r.msUntilExpiry)));
63
166
  err('');
64
167
  return 0;
65
168
  case 'refreshed':
66
- err(` ✅ 연결됨 — refresh_token으로 갱신 (${fmtRemaining(r.msUntilExpiry)})`);
169
+ err(M.statusRefreshed(fmtRemaining(r.msUntilExpiry)));
67
170
  err('');
68
171
  return 0;
69
172
  case 'expired_refresh_failed':
70
- err(' ⚠️ 토큰 만료 + 자동 갱신 실패');
173
+ err(M.statusExpired);
71
174
  printAuthError(r.error);
72
175
  err('');
73
176
  return 2;
74
177
  case 'unauthenticated':
75
- err(' ❌ 연결된 계정 없음.');
178
+ err(M.statusNone);
76
179
  printAuthError(r.error);
77
180
  err('');
78
181
  return 1;
@@ -80,25 +183,25 @@ async function cmdStatus() {
80
183
  }
81
184
  async function cmdRefresh() {
82
185
  err('');
83
- err(' 🔄 refresh_token으로 갱신 시도 중...');
186
+ err(M.refreshTrying);
84
187
  err('');
85
188
  const r = await ensureFreshAccessToken(0); // 무조건 갱신 시도
86
189
  switch (r.status) {
87
190
  case 'fresh':
88
- err(` ✅ 토큰 유효 — 갱신 불필요 (${fmtRemaining(r.msUntilExpiry)})`);
191
+ err(M.refreshNotNeeded(fmtRemaining(r.msUntilExpiry)));
89
192
  err('');
90
193
  return 0;
91
194
  case 'refreshed':
92
- err(` ✅ 갱신 완료 (${fmtRemaining(r.msUntilExpiry)})`);
195
+ err(M.refreshDone(fmtRemaining(r.msUntilExpiry)));
93
196
  err('');
94
197
  return 0;
95
198
  case 'expired_refresh_failed':
96
- err(' ❌ refresh 실패');
199
+ err(M.refreshFailed);
97
200
  printAuthError(r.error);
98
201
  err('');
99
202
  return 2;
100
203
  case 'unauthenticated':
101
- err(' ❌ 저장된 토큰 없음.');
204
+ err(M.refreshNoToken);
102
205
  printAuthError(r.error);
103
206
  err('');
104
207
  return 1;
@@ -112,10 +215,10 @@ async function cmdLogout() {
112
215
  err('');
113
216
  if (fs.existsSync(tokenPath)) {
114
217
  fs.rmSync(tokenPath, { force: true });
115
- err(' ✅ 토큰 삭제 완료.');
218
+ err(M.logoutDone);
116
219
  }
117
220
  else {
118
- err(' (이미 삭제된 상태)');
221
+ err(M.logoutAlready);
119
222
  }
120
223
  err('');
121
224
  return 0;
@@ -125,20 +228,20 @@ async function cmdLogin() {
125
228
  const force = hasFlag('force');
126
229
  const timeoutSec = parseInt(flagValue('timeout') ?? '600', 10);
127
230
  err('');
128
- err(' ☕ Mimi Seed — Google 계정 연결');
231
+ err(M.loginTitle);
129
232
  err('');
130
233
  // 1) 기존 토큰이 있으면 silent refresh 먼저 시도
131
234
  if (!force) {
132
235
  const existing = getStoredTokens();
133
236
  if (existing) {
134
- err(' 🔍 기존 토큰 검사 중...');
237
+ err(M.loginChecking);
135
238
  const r = await ensureFreshAccessToken();
136
239
  if (r.status === 'fresh' || r.status === 'refreshed') {
137
- const label = r.status === 'fresh' ? '유효함' : 'refresh_token으로 갱신 완료';
138
- err(` ✅ 이미 연결됨 (${label}, ${fmtRemaining(r.msUntilExpiry)}).`);
240
+ const label = r.status === 'fresh' ? M.loginValid : M.loginRefreshed;
241
+ err(M.loginAlready(label, fmtRemaining(r.msUntilExpiry)));
139
242
  err('');
140
243
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
141
- const answer = await new Promise((res) => rl.question(' 다시 로그인할래? (y/N): ', res));
244
+ const answer = await new Promise((res) => rl.question(M.loginAgain, res));
142
245
  rl.close();
143
246
  if (answer.toLowerCase() !== 'y') {
144
247
  err('');
@@ -146,7 +249,7 @@ async function cmdLogin() {
146
249
  }
147
250
  }
148
251
  else if (r.status === 'expired_refresh_failed') {
149
- err(` ⚠️ 토큰 만료 + 자동 갱신 실패 [${r.error.code}] — 재로그인 진행.`);
252
+ err(M.loginExpiredRelogin(r.error.code));
150
253
  if (r.error.cause && process.env.DEBUG)
151
254
  err(` cause: ${r.error.cause}`);
152
255
  }
@@ -154,7 +257,7 @@ async function cmdLogin() {
154
257
  }
155
258
  // 2) OAuth 콜백 서버 + URL 발급
156
259
  err('');
157
- err(' 🌐 OAuth 콜백 서버 시작: http://localhost:9876/callback');
260
+ err(M.serverStart);
158
261
  let url;
159
262
  let wait;
160
263
  try {
@@ -166,7 +269,7 @@ async function cmdLogin() {
166
269
  wait = r.wait;
167
270
  }
168
271
  catch (e) {
169
- err(' ❌ 콜백 서버 시작 실패');
272
+ err(M.serverFail);
170
273
  printAuthError(classifyError(e, { phase: 'login' }));
171
274
  err('');
172
275
  return 1;
@@ -174,25 +277,25 @@ async function cmdLogin() {
174
277
  // 3) 브라우저 열기 (or URL 출력)
175
278
  if (noBrowser) {
176
279
  err('');
177
- err(' 📋 아래 URL을 브라우저에 직접 붙여넣으세요:');
280
+ err(M.pasteUrl);
178
281
  err('');
179
282
  err(' ' + url);
180
283
  err('');
181
284
  }
182
285
  else {
183
- err(' 🌐 기본 브라우저 자동 열기...');
286
+ err(M.openingBrowser);
184
287
  try {
185
288
  await open(url);
186
- err(' (실패 시 --no-browser 로 URL 직접 받기)');
289
+ err(M.openingHint);
187
290
  }
188
291
  catch (e) {
189
- err(' ⚠️ 브라우저 자동 열기 실패: ' + (e instanceof Error ? e.message : String(e)));
190
- err(' 📋 직접 열어주세요:');
292
+ err(M.openFail(e instanceof Error ? e.message : String(e)));
293
+ err(M.openManually);
191
294
  err(' ' + url);
192
295
  }
193
296
  }
194
297
  // 4) 콜백 대기
195
- err(` ⏳ Google 승인 대기 중... (timeout ${timeoutSec}s)`);
298
+ err(M.waiting(timeoutSec));
196
299
  // 진행 표시기 — 사용자에게 살아있다는 신호 전달
197
300
  const ticker = setInterval(() => process.stderr.write('.'), 5000);
198
301
  try {
@@ -201,7 +304,7 @@ async function cmdLogin() {
201
304
  catch (e) {
202
305
  clearInterval(ticker);
203
306
  err('');
204
- err(' ❌ 인증 실패');
307
+ err(M.authFailed);
205
308
  if (e instanceof AuthError) {
206
309
  printAuthError(e.payload);
207
310
  }
@@ -214,12 +317,12 @@ async function cmdLogin() {
214
317
  clearInterval(ticker);
215
318
  err('');
216
319
  err('');
217
- err(' ✅ 연결 완료!');
320
+ err(M.done);
218
321
  err('');
219
- err(' 이제 Claude Code 또는 Codex에서 이렇게 쓸 수 있어:');
220
- err(' "내 Firebase 프로젝트 보여줘"');
221
- err(' "새 Android 앱 등록해줘"');
222
- err(' "google-services.json 다운로드해줘"');
322
+ err(M.nextTitle);
323
+ err(M.nextExample1);
324
+ err(M.nextExample2);
325
+ err(M.nextExample3);
223
326
  err('');
224
327
  return 0;
225
328
  }
@@ -241,7 +344,7 @@ async function main() {
241
344
  }
242
345
  main().catch((e) => {
243
346
  err('');
244
- err(' ❌ 예외: ' + (e instanceof Error ? e.message : String(e)));
347
+ err(M.fatal(e instanceof Error ? e.message : String(e)));
245
348
  err('');
246
349
  process.exit(1);
247
350
  });