@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.
@@ -2,36 +2,83 @@
2
2
  import { saveServiceAccountJson, getServiceAccountJson } from './playstore-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 — Google Play 서비스 계정 연결',
9
+ already: (email) => ` ✅ 이미 연결됨 (${email})`,
10
+ existingBroken: ' ⚠️ 저장된 서비스 계정이 있지만 파싱 오류',
11
+ reconnect: ' 다시 설정할래? (y/N): ',
12
+ needTitle: ' Google Play 서비스 계정 키 JSON 파일이 필요해:',
13
+ step1: ' 1. Google Cloud Console → IAM & Admin → Service Accounts',
14
+ step2: ' 2. 서비스 계정 선택 → Keys → Add Key → Create new key → JSON',
15
+ step3: ' 3. 다운로드한 JSON 파일 경로를 입력해',
16
+ askPath: ' 서비스 계정 JSON 파일 경로: ',
17
+ noFile: (p) => ` ❌ 파일 없음: ${p}`,
18
+ badJson: ' ❌ JSON 파싱 실패 — 올바른 서비스 계정 키 파일인지 확인해줘',
19
+ notServiceAccount: ' ❌ 서비스 계정 JSON 형식이 아니야.',
20
+ notServiceAccountFields: ' type="service_account", client_email, private_key 필드가 있어야 해.',
21
+ saved: (email) => ` ✅ 저장 완료! (${email})`,
22
+ nextTitle: ' 이제 Claude Code 또는 Codex에서:',
23
+ nextExample1: ' "내 Play 스토어 앱 리스팅 보여줘"',
24
+ nextExample2: ' "Play 구독 상품 목록 보여줘"',
25
+ permWarn: ' ⚠️ Play Console 권한 확인:',
26
+ permStep1: ' Play Console → Users and permissions → 이 서비스 계정 추가',
27
+ permStep2: ' 앱 권한에서 "View financial data" + "Manage store listing" 체크',
28
+ };
29
+ const en = {
30
+ title: ' 🤖 Mimi Seed — Connect a Google Play service account',
31
+ already: (email) => ` ✅ Already connected (${email})`,
32
+ existingBroken: ' ⚠️ A service account is saved, but it failed to parse',
33
+ reconnect: ' Set it up again? (y/N): ',
34
+ needTitle: ' A Google Play service-account key JSON file is required:',
35
+ step1: ' 1. Google Cloud Console → IAM & Admin → Service Accounts',
36
+ step2: ' 2. Pick a service account → Keys → Add Key → Create new key → JSON',
37
+ step3: ' 3. Enter the path to the downloaded JSON file',
38
+ askPath: ' Path to the service-account JSON file: ',
39
+ noFile: (p) => ` ❌ File not found: ${p}`,
40
+ badJson: ' ❌ Failed to parse JSON — check that this is a valid service-account key file',
41
+ notServiceAccount: ' ❌ This is not a service-account JSON.',
42
+ notServiceAccountFields: ' It must contain type="service_account", client_email and private_key fields.',
43
+ saved: (email) => ` ✅ Saved! (${email})`,
44
+ nextTitle: ' Now, in Claude Code or Codex:',
45
+ nextExample1: ' "Show my Play Store listing"',
46
+ nextExample2: ' "List my Play subscriptions"',
47
+ permWarn: ' ⚠️ Check the Play Console permissions:',
48
+ permStep1: ' Play Console → Users and permissions → invite this service account',
49
+ permStep2: ' In the app permissions, check "View financial data" + "Manage store listing"',
50
+ };
51
+ const M = resolveLang() === 'en' ? en : ko;
5
52
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
6
53
  const ask = (q) => new Promise((r) => rl.question(q, r));
7
54
  async function main() {
8
55
  console.log('');
9
- console.log(' 🤖 Mimi Seed — Google Play 서비스 계정 연결');
56
+ console.log(M.title);
10
57
  console.log('');
11
58
  const existing = getServiceAccountJson();
12
59
  if (existing) {
13
60
  try {
14
61
  const parsed = JSON.parse(existing);
15
- console.log(` ✅ 이미 연결됨 (${parsed.client_email ?? '?'})`);
62
+ console.log(M.already(parsed.client_email ?? '?'));
16
63
  }
17
64
  catch {
18
- console.log(' ⚠️ 저장된 서비스 계정이 있지만 파싱 오류');
65
+ console.log(M.existingBroken);
19
66
  }
20
- const answer = await ask(' 다시 설정할래? (y/N): ');
67
+ const answer = await ask(M.reconnect);
21
68
  if (answer.toLowerCase() !== 'y') {
22
69
  rl.close();
23
70
  return;
24
71
  }
25
72
  }
26
- console.log(' Google Play 서비스 계정 키 JSON 파일이 필요해:');
27
- console.log(' 1. Google Cloud Console → IAM & Admin → Service Accounts');
28
- console.log(' 2. 서비스 계정 선택 → Keys → Add Key → Create new key → JSON');
29
- console.log(' 3. 다운로드한 JSON 파일 경로를 입력해');
73
+ console.log(M.needTitle);
74
+ console.log(M.step1);
75
+ console.log(M.step2);
76
+ console.log(M.step3);
30
77
  console.log('');
31
- const jsonPath = await ask(' 서비스 계정 JSON 파일 경로: ');
78
+ const jsonPath = await ask(M.askPath);
32
79
  const trimmedPath = jsonPath.trim().replace(/^["']|["']$/g, '');
33
80
  if (!fs.existsSync(trimmedPath)) {
34
- console.log(` ❌ 파일 없음: ${trimmedPath}`);
81
+ console.log(M.noFile(trimmedPath));
35
82
  rl.close();
36
83
  process.exit(1);
37
84
  }
@@ -41,27 +88,27 @@ async function main() {
41
88
  parsed = JSON.parse(json);
42
89
  }
43
90
  catch {
44
- console.log(' ❌ JSON 파싱 실패 — 올바른 서비스 계정 키 파일인지 확인해줘');
91
+ console.log(M.badJson);
45
92
  rl.close();
46
93
  process.exit(1);
47
94
  }
48
95
  if (parsed.type !== 'service_account' || !parsed.client_email || !parsed.private_key) {
49
- console.log(' ❌ 서비스 계정 JSON 형식이 아니야.');
50
- console.log(' type="service_account", client_email, private_key 필드가 있어야 해.');
96
+ console.log(M.notServiceAccount);
97
+ console.log(M.notServiceAccountFields);
51
98
  rl.close();
52
99
  process.exit(1);
53
100
  }
54
101
  saveServiceAccountJson(json);
55
102
  console.log('');
56
- console.log(` ✅ 저장 완료! (${parsed.client_email})`);
103
+ console.log(M.saved(parsed.client_email));
57
104
  console.log('');
58
- console.log(' 이제 Claude Code 또는 Codex에서:');
59
- console.log(' "내 Play 스토어 앱 리스팅 보여줘"');
60
- console.log(' "Play 구독 상품 목록 보여줘"');
105
+ console.log(M.nextTitle);
106
+ console.log(M.nextExample1);
107
+ console.log(M.nextExample2);
61
108
  console.log('');
62
- console.log(' ⚠️ Play Console 권한 확인:');
63
- console.log(' Play Console → Users and permissions → 이 서비스 계정 추가');
64
- console.log(' 앱 권한에서 "View financial data" + "Manage store listing" 체크');
109
+ console.log(M.permWarn);
110
+ console.log(M.permStep1);
111
+ console.log(M.permStep2);
65
112
  console.log('');
66
113
  rl.close();
67
114
  }
@@ -0,0 +1,6 @@
1
+ export interface ConnectResult {
2
+ ok: boolean;
3
+ /** 사용자에게 그대로 보여줄 메시지 (MCP 는 content 로 감싸고, CLI 는 그대로 출력). */
4
+ text: string;
5
+ }
6
+ export declare function connectFacebook(pageAccessToken: string, pageId?: string): Promise<ConnectResult>;
@@ -0,0 +1,72 @@
1
+ // Facebook 페이지 연결 — pageId 자동 조회 → 토큰 검증 → 검증 성공 시에만 저장.
2
+ //
3
+ // 이 로직은 MCP 도구(facebook_save_config)와 setup CLI(mimi-seed-social-auth)가 **공유**한다.
4
+ // 둘 중 하나에만 검증이 있으면 "CLI 로 저장했는데 도구가 못 읽는" 류의 드리프트가 생긴다.
5
+ import { saveFacebookConfig } from './config.js';
6
+ import * as api from './api.js';
7
+ const SIXTY_DAYS_MS = 60 * 24 * 60 * 60 * 1000;
8
+ export async function connectFacebook(pageAccessToken, pageId) {
9
+ let resolvedPageId = pageId;
10
+ if (!resolvedPageId) {
11
+ try {
12
+ const pages = await api.listAccessiblePages(pageAccessToken);
13
+ if (pages.length === 0) {
14
+ // 이미 Page Access Token 인 경우 — /me 가 곧 페이지다.
15
+ const res = await fetch(`https://graph.facebook.com/v21.0/me?fields=id,name&access_token=${pageAccessToken}`);
16
+ const data = (await res.json());
17
+ if (data.error)
18
+ throw new Error(`${data.error.message} (code ${data.error.code})`);
19
+ resolvedPageId = data.id;
20
+ }
21
+ else if (pages.length === 1) {
22
+ resolvedPageId = pages[0].id;
23
+ }
24
+ else {
25
+ const list = pages.map((p) => ` • ${p.name} (${p.id})`).join('\n');
26
+ return {
27
+ ok: false,
28
+ text: ['여러 페이지에 접근 가능합니다. pageId를 명시해주세요:', list].join('\n'),
29
+ };
30
+ }
31
+ }
32
+ catch (err) {
33
+ return { ok: false, text: `❌ pageId 자동 조회 실패: ${err.message}` };
34
+ }
35
+ }
36
+ try {
37
+ const page = await api.getPage({ pageAccessToken, pageId: resolvedPageId });
38
+ saveFacebookConfig({
39
+ pageAccessToken,
40
+ pageId: resolvedPageId,
41
+ pageName: page.name,
42
+ expiresAt: new Date(Date.now() + SIXTY_DAYS_MS).toISOString(),
43
+ });
44
+ return {
45
+ ok: true,
46
+ text: [
47
+ '✅ Facebook 페이지 연결 완료',
48
+ ` 페이지: ${page.name}`,
49
+ ` ID: ${page.id}`,
50
+ page.category ? ` 카테고리: ${page.category}` : '',
51
+ page.followers_count !== undefined
52
+ ? ` 팔로워: ${page.followers_count.toLocaleString()}`
53
+ : '',
54
+ page.fan_count !== undefined ? ` 좋아요: ${page.fan_count.toLocaleString()}` : '',
55
+ ]
56
+ .filter(Boolean)
57
+ .join('\n'),
58
+ };
59
+ }
60
+ catch (err) {
61
+ // 검증 실패 → 저장하지 않는다.
62
+ return {
63
+ ok: false,
64
+ text: [
65
+ '❌ 토큰 검증 실패',
66
+ ` ${err.message}`,
67
+ '',
68
+ 'Page Access Token을 다시 확인하거나 facebook_list_pages로 페이지 목록을 조회하세요.',
69
+ ].join('\n'),
70
+ };
71
+ }
72
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+ // mimi-seed-googleads-auth — Google Ads 연결 설정 (~/.mimi-seed/google-ads.json).
3
+ //
4
+ // Ads 는 Google OAuth 토큰의 `adwords` 스코프를 타므로, 개발자 토큰만 있어도 스코프가 없으면
5
+ // 실패한다. 저장 후 listAccessibleCustomers 로 **실제 호출**해서 그 경우를 여기서 잡아낸다.
6
+ import readline from 'node:readline';
7
+ import { loadConfig, saveConfig, normalizeCustomerId } from './config.js';
8
+ import { listAccessibleCustomers } from './tools.js';
9
+ import { getAuthenticatedClient } from '../auth/google-auth.js';
10
+ import { resolveLang } from '../lib/lang.js';
11
+ // ko 가 원본이고 en 은 `typeof ko` 를 만족해야 한다 — 키를 빠뜨리면 컴파일이 깨진다.
12
+ const ko = {
13
+ title: ' 🤖 Mimi Seed — Google Ads 연결',
14
+ already: (customerId) => ` ✅ 이미 연결됨 (customerId: ${customerId})`,
15
+ reconnect: ' 다시 설정할래? (y/N): ',
16
+ needTitle: ' 필요한 것 2가지:',
17
+ needToken: ' 1. Developer Token — Google Ads → 도구 및 설정 → 설정 → API 센터',
18
+ needTokenWarn: ' ⚠️ 최초 발급은 "테스트" 등급이고, 실계정 조회는 **승인 심사**를 거쳐야 해.',
19
+ needCustomerId: ' 2. Customer ID — Google Ads 우상단 계정 번호 (예: 123-456-7890)',
20
+ askToken: ' Developer Token: ',
21
+ askCustomerId: ' Customer ID (예: 123-456-7890): ',
22
+ askLoginCustomerId: ' MCC(관리자) 계정 ID (선택, 엔터 스킵): ',
23
+ required: ' ❌ Developer Token 과 Customer ID 는 필수야.',
24
+ noOauth: ' ❌ Google OAuth 토큰이 없어 검증할 수 없어. 저장하지 않았어.',
25
+ noOauthFix: ' 먼저: npx -y @yoonion/mimi-seed-mcp mimi-seed-auth',
26
+ probing: ' 🔎 접근 가능한 고객 계정 조회 중...',
27
+ probeOk: (n) => ` ✅ 접근 OK — 계정 ${n}개`,
28
+ probeFail: (msg) => ` ❌ 검증 실패: ${msg}`,
29
+ hintScope: ' → OAuth 토큰에 `adwords` 스코프가 없을 가능성이 커.',
30
+ hintScopeFix: ' 재인증: npx -y @yoonion/mimi-seed-mcp mimi-seed-auth --force',
31
+ hintOther: ' → Developer Token 등급(테스트/승인)과 Customer ID 를 다시 확인해줘.',
32
+ notSaved: ' 저장하지 않았어 (기존 설정이 있으면 그대로 유지된다).',
33
+ saved: ' ✅ 저장 완료 → ~/.mimi-seed/google-ads.json',
34
+ };
35
+ const en = {
36
+ title: ' 🤖 Mimi Seed — Connect Google Ads',
37
+ already: (customerId) => ` ✅ Already connected (customerId: ${customerId})`,
38
+ reconnect: ' Set it up again? (y/N): ',
39
+ needTitle: ' Two things are needed:',
40
+ needToken: ' 1. Developer Token — Google Ads → Tools and settings → Setup → API Center',
41
+ needTokenWarn: ' ⚠️ A new token starts at "Test" access level; querying real accounts requires **API-Center approval**.',
42
+ needCustomerId: ' 2. Customer ID — the account number at the top right of Google Ads (e.g. 123-456-7890)',
43
+ askToken: ' Developer Token: ',
44
+ askCustomerId: ' Customer ID (e.g. 123-456-7890): ',
45
+ askLoginCustomerId: ' MCC (manager) account ID (optional, Enter to skip): ',
46
+ required: ' ❌ Developer Token and Customer ID are required.',
47
+ noOauth: ' ❌ No Google OAuth token, so this cannot be verified. Nothing was saved.',
48
+ noOauthFix: ' First run: npx -y @yoonion/mimi-seed-mcp mimi-seed-auth',
49
+ probing: ' 🔎 Listing accessible customer accounts...',
50
+ probeOk: (n) => ` ✅ Access OK — ${n} account(s)`,
51
+ probeFail: (msg) => ` ❌ Verification failed: ${msg}`,
52
+ hintScope: ' → Your OAuth token most likely lacks the `adwords` scope.',
53
+ hintScopeFix: ' Re-authenticate: npx -y @yoonion/mimi-seed-mcp mimi-seed-auth --force',
54
+ hintOther: ' → Double-check the Developer Token access level (test / approved) and the Customer ID.',
55
+ notSaved: ' Nothing was saved (an existing config is left untouched).',
56
+ saved: ' ✅ Saved → ~/.mimi-seed/google-ads.json',
57
+ };
58
+ const M = resolveLang() === 'en' ? en : ko;
59
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
60
+ const ask = (q) => new Promise((r) => rl.question(q, (a) => r(a.trim())));
61
+ async function main() {
62
+ console.log('');
63
+ console.log(M.title);
64
+ console.log('');
65
+ const existing = loadConfig();
66
+ if (existing) {
67
+ console.log(M.already(existing.customerId));
68
+ const answer = await ask(M.reconnect);
69
+ if (answer.toLowerCase() !== 'y') {
70
+ rl.close();
71
+ return;
72
+ }
73
+ }
74
+ console.log(M.needTitle);
75
+ console.log(M.needToken);
76
+ console.log(M.needTokenWarn);
77
+ console.log(M.needCustomerId);
78
+ console.log('');
79
+ const developerToken = await ask(M.askToken);
80
+ const customerId = await ask(M.askCustomerId);
81
+ const loginCustomerId = await ask(M.askLoginCustomerId);
82
+ if (!developerToken || !customerId) {
83
+ console.log(M.required);
84
+ rl.close();
85
+ process.exit(1);
86
+ }
87
+ const cfg = {
88
+ developerToken,
89
+ customerId: normalizeCustomerId(customerId),
90
+ loginCustomerId: loginCustomerId ? normalizeCustomerId(loginCustomerId) : undefined,
91
+ };
92
+ // 검증을 **저장보다 먼저** 한다 (jenkins/facebook/instagram 과 같은 규율).
93
+ // 먼저 저장해버리면 (a) 403 나는 설정인데 doctor 가 ✓ 로 표시하고,
94
+ // (b) 오타 한 번에 잘 되던 기존 설정이 백업 없이 덮여 날아간다.
95
+ const auth = getAuthenticatedClient();
96
+ if (!auth) {
97
+ console.log('');
98
+ console.log(M.noOauth);
99
+ console.log(M.noOauthFix);
100
+ rl.close();
101
+ process.exit(1);
102
+ }
103
+ console.log('');
104
+ console.log(M.probing);
105
+ try {
106
+ const customers = await listAccessibleCustomers(auth, cfg);
107
+ console.log(M.probeOk(customers.length));
108
+ }
109
+ catch (e) {
110
+ const msg = e instanceof Error ? e.message : String(e);
111
+ console.log(M.probeFail(msg));
112
+ console.log('');
113
+ if (/scope|insufficient|PERMISSION_DENIED/i.test(msg)) {
114
+ console.log(M.hintScope);
115
+ console.log(M.hintScopeFix);
116
+ }
117
+ else {
118
+ console.log(M.hintOther);
119
+ }
120
+ console.log('');
121
+ console.log(M.notSaved);
122
+ rl.close();
123
+ process.exit(1);
124
+ }
125
+ saveConfig(cfg);
126
+ console.log('');
127
+ console.log(M.saved);
128
+ console.log('');
129
+ rl.close();
130
+ }
131
+ main().catch((e) => {
132
+ console.error(`\n ❌ ${e instanceof Error ? e.message : String(e)}\n`);
133
+ rl.close();
134
+ process.exit(1);
135
+ });
package/dist/index.js CHANGED
@@ -14,6 +14,9 @@ const SUBCOMMANDS = {
14
14
  'mimi-seed-playstore-auth': () => import('./auth/playstore-setup-cli.js'),
15
15
  'mimi-seed-appstore-auth': () => import('./appstore/setup-cli.js'),
16
16
  'mimi-seed-bigquery-auth': () => import('./auth/bigquery-setup-cli.js'),
17
+ 'mimi-seed-jenkins-auth': () => import('./jenkins/setup-cli.js'),
18
+ 'mimi-seed-googleads-auth': () => import('./googleads/setup-cli.js'),
19
+ 'mimi-seed-social-auth': () => import('./social/setup-cli.js'),
17
20
  'mimi-seed-firebase': () => import('./firebase/cli.js'),
18
21
  'mimi-seed-admob': () => import('./admob/cli.js'),
19
22
  'mimi-seed-ga4': () => import('./ga4/cli.js'),
@@ -0,0 +1,4 @@
1
+ import type { ConnectResult } from '../facebook/setup.js';
2
+ /** IGAA… = Instagram Login(신규) · EAA… = Facebook Login(FB Page + IG Business 연결 필요) */
3
+ export declare function detectApiType(accessToken: string): string;
4
+ export declare function connectInstagram(accessToken: string, userId?: string, assumeIssuedNow?: boolean): Promise<ConnectResult>;
@@ -0,0 +1,70 @@
1
+ // Instagram 연결 — userId 자동 조회 → 토큰 검증 → 검증 성공 시에만 저장.
2
+ // MCP 도구(instagram_save_config)와 setup CLI(mimi-seed-social-auth)가 **공유**하는 구현.
3
+ import { saveInstagramConfig } from './config.js';
4
+ import * as api from './api.js';
5
+ const SIXTY_DAYS_MS = 60 * 24 * 60 * 60 * 1000;
6
+ /** IGAA… = Instagram Login(신규) · EAA… = Facebook Login(FB Page + IG Business 연결 필요) */
7
+ export function detectApiType(accessToken) {
8
+ return accessToken.startsWith('IGAA') ? 'Instagram Login' : 'Facebook Login';
9
+ }
10
+ export async function connectInstagram(accessToken, userId, assumeIssuedNow = true) {
11
+ const apiType = detectApiType(accessToken);
12
+ let resolvedUserId = userId;
13
+ if (!resolvedUserId) {
14
+ try {
15
+ resolvedUserId = await api.fetchUserId(accessToken);
16
+ }
17
+ catch (err) {
18
+ return {
19
+ ok: false,
20
+ text: [
21
+ `❌ userId 자동 조회 실패 (${apiType})`,
22
+ ` ${err.message}`,
23
+ '',
24
+ 'userId를 명시적으로 전달하거나 토큰을 다시 확인하세요.',
25
+ ].join('\n'),
26
+ };
27
+ }
28
+ }
29
+ const expiresAt = assumeIssuedNow
30
+ ? new Date(Date.now() + SIXTY_DAYS_MS).toISOString()
31
+ : undefined;
32
+ try {
33
+ const account = await api.getAccount({ accessToken, userId: resolvedUserId });
34
+ saveInstagramConfig({
35
+ accessToken,
36
+ userId: resolvedUserId,
37
+ expiresAt,
38
+ username: account.username,
39
+ });
40
+ return {
41
+ ok: true,
42
+ text: [
43
+ `✅ Instagram 연결 확인 완료 (${apiType})`,
44
+ ` 계정: @${account.username}${account.name ? ` (${account.name})` : ''}`,
45
+ ` ID: ${account.id}`,
46
+ account.account_type ? ` 타입: ${account.account_type}` : '',
47
+ account.followers_count !== undefined
48
+ ? ` 팔로워: ${account.followers_count.toLocaleString()}`
49
+ : '',
50
+ account.media_count !== undefined ? ` 게시물: ${account.media_count}` : '',
51
+ expiresAt ? ` 토큰 만료(추정): ${expiresAt.slice(0, 10)}` : '',
52
+ ]
53
+ .filter(Boolean)
54
+ .join('\n'),
55
+ };
56
+ }
57
+ catch (err) {
58
+ // 검증 실패 → 저장하지 않는다.
59
+ return {
60
+ ok: false,
61
+ text: [
62
+ `❌ 토큰 검증 실패 (${apiType})`,
63
+ ` userId: ${resolvedUserId}`,
64
+ ` ${err.message}`,
65
+ '',
66
+ '토큰을 다시 발급받거나 userId를 직접 지정해보세요.',
67
+ ].join('\n'),
68
+ };
69
+ }
70
+ }
@@ -2,7 +2,17 @@ export interface JenkinsConfig {
2
2
  url: string;
3
3
  username: string;
4
4
  token: string;
5
+ jobAndroid?: string;
6
+ jobIos?: string;
5
7
  }
6
8
  export declare function loadJenkinsConfig(): JenkinsConfig | null;
7
- export declare function saveJenkinsConfig(config: JenkinsConfig): void;
9
+ /**
10
+ * jenkins.json 저장 — **기존 값 위에 병합한다** (통째로 덮어쓰지 않는다).
11
+ *
12
+ * 이 파일에는 두 종류의 필드가 산다: 연결 정보(url/username/token)는 MCP 도구
13
+ * `jenkins_save_config` 가, 빌드 잡 이름(jobAndroid/jobIos)은 CLI 의 setup 이 쓴다.
14
+ * 통째로 덮어쓰면 한쪽이 다른 쪽 값을 지운다 — 예전에 Jenkins 설정이 config.json 과
15
+ * jenkins.json 으로 갈라졌던 것과 같은 종류의 사고다. undefined 인 필드는 건드리지 않는다.
16
+ */
17
+ export declare function saveJenkinsConfig(config: Partial<JenkinsConfig> & Pick<JenkinsConfig, 'url' | 'username' | 'token'>): void;
8
18
  export declare function requireJenkinsConfig(): JenkinsConfig;
@@ -11,11 +11,24 @@ export function loadJenkinsConfig() {
11
11
  return null;
12
12
  }
13
13
  }
14
+ /**
15
+ * jenkins.json 저장 — **기존 값 위에 병합한다** (통째로 덮어쓰지 않는다).
16
+ *
17
+ * 이 파일에는 두 종류의 필드가 산다: 연결 정보(url/username/token)는 MCP 도구
18
+ * `jenkins_save_config` 가, 빌드 잡 이름(jobAndroid/jobIos)은 CLI 의 setup 이 쓴다.
19
+ * 통째로 덮어쓰면 한쪽이 다른 쪽 값을 지운다 — 예전에 Jenkins 설정이 config.json 과
20
+ * jenkins.json 으로 갈라졌던 것과 같은 종류의 사고다. undefined 인 필드는 건드리지 않는다.
21
+ */
14
22
  export function saveJenkinsConfig(config) {
15
23
  if (!fs.existsSync(CONFIG_DIR)) {
16
24
  fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
17
25
  }
18
- fs.writeFileSync(JENKINS_CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
26
+ const existing = loadJenkinsConfig() ?? {};
27
+ const merged = { ...existing, ...stripUndefined(config) };
28
+ fs.writeFileSync(JENKINS_CONFIG_PATH, JSON.stringify(merged, null, 2), { mode: 0o600 });
29
+ }
30
+ function stripUndefined(o) {
31
+ return Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined));
19
32
  }
20
33
  export function requireJenkinsConfig() {
21
34
  const cfg = loadJenkinsConfig();
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env node
2
+ // mimi-seed-jenkins-auth — Jenkins 연결 설정 (~/.mimi-seed/jenkins.json).
3
+ //
4
+ // jenkins_save_config 도구와 달리 저장 **전에** 실제 서버를 프로브한다 (listCredentials).
5
+ // 잘못된 URL/토큰이 조용히 저장돼서 deploy 때 터지는 걸 막는다.
6
+ import readline from 'node:readline';
7
+ import { loadJenkinsConfig, saveJenkinsConfig } from './config.js';
8
+ import { listCredentials } from './credentials.js';
9
+ import { resolveLang } from '../lib/lang.js';
10
+ // ko 가 원본이고 en 은 `typeof ko` 를 만족해야 한다 — 키를 빠뜨리면 컴파일이 깨진다.
11
+ const ko = {
12
+ title: ' 🤖 Mimi Seed — Jenkins 연결',
13
+ already: (url) => ` ✅ 이미 연결됨 (${url})`,
14
+ reconnect: ' 다시 설정할래? (y/N): ',
15
+ howToToken: ' API Token 발급: Jenkins → [사용자 이름] → 설정 → API Token → "Add new Token"',
16
+ remoteOk: ' 로컬 Jenkins 가 없어도 회사·원격 서버 URL 을 그대로 쓰면 돼.',
17
+ askUrl: ' Jenkins URL (예: https://jenkins.company.com): ',
18
+ askUser: ' Jenkins 사용자 ID: ',
19
+ askToken: ' Jenkins API Token: ',
20
+ allRequired: ' ❌ URL / 사용자 ID / 토큰은 모두 필요해.',
21
+ probing: ' 🔎 연결 확인 중...',
22
+ probeOk: (n) => ` ✅ 연결 OK — 크리덴셜 ${n}개 조회됨`,
23
+ probeFail: (msg) => ` ❌ 연결 실패: ${msg}`,
24
+ checkTitle: ' 확인할 것:',
25
+ checkUrl: ' • URL 이 Jenkins 대시보드 주소와 같은지 (예: https://jenkins.company.com)',
26
+ checkToken: ' • 비밀번호가 아니라 **API Token** 을 넣었는지',
27
+ checkPerm: ' • 그 계정에 Credentials 조회 권한이 있는지',
28
+ notSaved: ' 저장하지 않았어. 값을 확인하고 다시 실행해줘.',
29
+ keepCurrent: (cur) => ` (현재: ${cur}, 엔터=유지)`,
30
+ keepOptional: ' (선택, 엔터 스킵)',
31
+ askJobAndroid: (keep) => ` Android 빌드 Job 이름${keep}: `,
32
+ askJobIos: (keep) => ` iOS 빌드 Job 이름${keep}: `,
33
+ saved: ' ✅ 저장 완료 → ~/.mimi-seed/jenkins.json',
34
+ savedHint: ' 확인: mimi-seed doctor · 빌드: mimi-seed deploy',
35
+ };
36
+ const en = {
37
+ title: ' 🤖 Mimi Seed — Connect Jenkins',
38
+ already: (url) => ` ✅ Already connected (${url})`,
39
+ reconnect: ' Set it up again? (y/N): ',
40
+ howToToken: ' Get an API Token: Jenkins → [your user name] → Configure → API Token → "Add new Token"',
41
+ remoteOk: ' No local Jenkins needed — a company / remote server URL works just as well.',
42
+ askUrl: ' Jenkins URL (e.g. https://jenkins.company.com): ',
43
+ askUser: ' Jenkins user ID: ',
44
+ askToken: ' Jenkins API Token: ',
45
+ allRequired: ' ❌ URL, user ID and token are all required.',
46
+ probing: ' 🔎 Checking the connection...',
47
+ probeOk: (n) => ` ✅ Connected — ${n} credential(s) listed`,
48
+ probeFail: (msg) => ` ❌ Connection failed: ${msg}`,
49
+ checkTitle: ' Things to check:',
50
+ checkUrl: ' • The URL matches your Jenkins dashboard address (e.g. https://jenkins.company.com)',
51
+ checkToken: ' • You entered an **API Token**, not your password',
52
+ checkPerm: ' • That account is allowed to read Credentials',
53
+ notSaved: ' Nothing was saved. Check the values and run this again.',
54
+ keepCurrent: (cur) => ` (current: ${cur}, Enter = keep)`,
55
+ keepOptional: ' (optional, Enter to skip)',
56
+ askJobAndroid: (keep) => ` Android build job name${keep}: `,
57
+ askJobIos: (keep) => ` iOS build job name${keep}: `,
58
+ saved: ' ✅ Saved → ~/.mimi-seed/jenkins.json',
59
+ savedHint: ' Check: mimi-seed doctor · Build: mimi-seed deploy',
60
+ };
61
+ const M = resolveLang() === 'en' ? en : ko;
62
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
63
+ const ask = (q) => new Promise((r) => rl.question(q, (a) => r(a.trim())));
64
+ async function main() {
65
+ console.log('');
66
+ console.log(M.title);
67
+ console.log('');
68
+ const existing = loadJenkinsConfig();
69
+ if (existing) {
70
+ console.log(M.already(existing.url));
71
+ const answer = await ask(M.reconnect);
72
+ if (answer.toLowerCase() !== 'y') {
73
+ rl.close();
74
+ return;
75
+ }
76
+ }
77
+ console.log(M.howToToken);
78
+ console.log(M.remoteOk);
79
+ console.log('');
80
+ const url = await ask(M.askUrl);
81
+ const username = await ask(M.askUser);
82
+ const token = await ask(M.askToken);
83
+ if (!url || !username || !token) {
84
+ console.log(M.allRequired);
85
+ rl.close();
86
+ process.exit(1);
87
+ }
88
+ console.log('');
89
+ console.log(M.probing);
90
+ const cfg = { url: url.replace(/\/+$/, ''), username, token };
91
+ try {
92
+ const creds = await listCredentials(cfg);
93
+ console.log(M.probeOk(creds.length));
94
+ }
95
+ catch (e) {
96
+ console.log(M.probeFail(e instanceof Error ? e.message : String(e)));
97
+ console.log('');
98
+ console.log(M.checkTitle);
99
+ console.log(M.checkUrl);
100
+ console.log(M.checkToken);
101
+ console.log(M.checkPerm);
102
+ console.log('');
103
+ console.log(M.notSaved);
104
+ rl.close();
105
+ process.exit(1);
106
+ }
107
+ // deploy 가 트리거할 잡 이름 (선택) — 같은 파일에 함께 저장한다.
108
+ // 재설정(토큰 교체 등) 시 엔터로 넘기면 **기존 값을 유지한다** — 조용히 지워버리면
109
+ // 다음 `mimi-seed deploy` 가 "job 이 설정되지 않았습니다" 로 죽는다.
110
+ const keep = (cur) => (cur ? M.keepCurrent(cur) : M.keepOptional);
111
+ const jobAndroid = await ask(M.askJobAndroid(keep(existing?.jobAndroid)));
112
+ const jobIos = await ask(M.askJobIos(keep(existing?.jobIos)));
113
+ saveJenkinsConfig({
114
+ ...cfg,
115
+ jobAndroid: jobAndroid || existing?.jobAndroid,
116
+ jobIos: jobIos || existing?.jobIos,
117
+ });
118
+ console.log('');
119
+ console.log(M.saved);
120
+ console.log(M.savedHint);
121
+ console.log('');
122
+ rl.close();
123
+ }
124
+ main().catch((e) => {
125
+ console.error(`\n ❌ ${e instanceof Error ? e.message : String(e)}\n`);
126
+ rl.close();
127
+ process.exit(1);
128
+ });
@@ -0,0 +1,5 @@
1
+ export type Lang = 'ko' | 'en';
2
+ export declare const DEFAULT_LANG: Lang;
3
+ export declare function isLang(v: unknown): v is Lang;
4
+ /** 우선순위: 환경변수 > ~/.mimi-seed/settings.json > 기본값(ko). 파일이 없거나 깨져도 ko 로 폴백. */
5
+ export declare function resolveLang(home?: string): Lang;
@@ -0,0 +1,33 @@
1
+ // 인터랙티브 setup bin 의 출력 언어.
2
+ //
3
+ // CLI 쪽 `packages/cli/src/settings.ts` 의 resolveLang 과 **같은 규칙**이어야 한다:
4
+ // MIMI_SEED_LANG 환경변수 > ~/.mimi-seed/settings.json { lang } > 'ko'.
5
+ //
6
+ // 환경변수가 1순위인 이유: CLI 마법사가 이 패키지의 setup bin 을 spawn 할 때 MIMI_SEED_LANG 을
7
+ // 물려준다. 마법사와 자식 프로세스의 언어가 어긋나면 온보딩 도중에 언어가 뒤섞인다.
8
+ //
9
+ // MCP 도구의 description / 도구 출력 텍스트는 여기 대상이 아니다 — 그건 사람이 아니라 LLM 이
10
+ // 읽는 인터페이스다. 이 모듈은 **터미널에 찍히는 사람용 문자열**에만 쓴다.
11
+ import fs from 'node:fs';
12
+ import os from 'node:os';
13
+ import path from 'node:path';
14
+ export const DEFAULT_LANG = 'ko';
15
+ export function isLang(v) {
16
+ return v === 'ko' || v === 'en';
17
+ }
18
+ /** 우선순위: 환경변수 > ~/.mimi-seed/settings.json > 기본값(ko). 파일이 없거나 깨져도 ko 로 폴백. */
19
+ export function resolveLang(home = os.homedir()) {
20
+ const env = process.env.MIMI_SEED_LANG?.toLowerCase();
21
+ if (isLang(env))
22
+ return env;
23
+ try {
24
+ const raw = fs.readFileSync(path.join(home, '.mimi-seed', 'settings.json'), 'utf-8');
25
+ const saved = JSON.parse(raw).lang;
26
+ if (isLang(saved))
27
+ return saved;
28
+ }
29
+ catch {
30
+ // 파일 없음 / 권한 없음 / JSON 깨짐 — 전부 기본값으로.
31
+ }
32
+ return DEFAULT_LANG;
33
+ }