mimi-seed 0.5.0 → 0.6.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.
@@ -3,28 +3,74 @@
3
3
  // src/setup.ts
4
4
  import kleur2 from "kleur";
5
5
  import * as readline2 from "readline";
6
- import os5 from "os";
6
+ import os6 from "os";
7
7
 
8
8
  // src/credentials.ts
9
+ import fs2 from "fs";
10
+ import os2 from "os";
11
+ import path2 from "path";
12
+
13
+ // src/settings.ts
9
14
  import fs from "fs";
10
15
  import os from "os";
11
16
  import path from "path";
17
+ var DEFAULT_LANG = "ko";
18
+ function settingsPath(home) {
19
+ return path.join(home, ".mimi-seed", "settings.json");
20
+ }
21
+ function readSettings(home = os.homedir()) {
22
+ try {
23
+ return JSON.parse(fs.readFileSync(settingsPath(home), "utf-8"));
24
+ } catch {
25
+ return {};
26
+ }
27
+ }
28
+ function writeSettings(next, home = os.homedir()) {
29
+ const dir = path.join(home, ".mimi-seed");
30
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
31
+ const merged = { ...readSettings(home), ...next };
32
+ fs.writeFileSync(settingsPath(home), JSON.stringify(merged, null, 2));
33
+ }
34
+ function isLangUnset(home = os.homedir()) {
35
+ return !process.env.MIMI_SEED_LANG && !readSettings(home).lang;
36
+ }
37
+ function isLang(v) {
38
+ return v === "ko" || v === "en";
39
+ }
40
+ function resolveLang(home = os.homedir()) {
41
+ const env = process.env.MIMI_SEED_LANG?.toLowerCase();
42
+ if (isLang(env)) return env;
43
+ const saved = readSettings(home).lang;
44
+ if (isLang(saved)) return saved;
45
+ return DEFAULT_LANG;
46
+ }
47
+
48
+ // src/credentials.ts
49
+ function credLabel(spec, lang = resolveLang()) {
50
+ return spec.label[lang];
51
+ }
52
+ function credNote(spec, lang = resolveLang()) {
53
+ return spec.note?.[lang];
54
+ }
55
+ function credObtain(spec, lang = resolveLang()) {
56
+ return spec.obtain[lang];
57
+ }
12
58
  function credDir(home) {
13
- return path.join(home, ".mimi-seed");
59
+ return path2.join(home, ".mimi-seed");
14
60
  }
15
61
  function hasFile(home, name) {
16
- return fs.existsSync(path.join(credDir(home), name));
62
+ return fs2.existsSync(path2.join(credDir(home), name));
17
63
  }
18
64
  function anyFileStarting(home, prefix) {
19
65
  try {
20
- return fs.readdirSync(credDir(home)).some((f) => f.startsWith(prefix));
66
+ return fs2.readdirSync(credDir(home)).some((f) => f.startsWith(prefix));
21
67
  } catch {
22
68
  return false;
23
69
  }
24
70
  }
25
71
  function readJson(home, name) {
26
72
  try {
27
- return JSON.parse(fs.readFileSync(path.join(credDir(home), name), "utf-8"));
73
+ return JSON.parse(fs2.readFileSync(path2.join(credDir(home), name), "utf-8"));
28
74
  } catch {
29
75
  return null;
30
76
  }
@@ -32,7 +78,7 @@ function readJson(home, name) {
32
78
  function hasPlaySa(home) {
33
79
  if (hasFile(home, "play-service-account.json")) return true;
34
80
  try {
35
- return fs.readdirSync(path.join(credDir(home), "play-service-accounts")).some((f) => f.endsWith(".json"));
81
+ return fs2.readdirSync(path2.join(credDir(home), "play-service-accounts")).some((f) => f.endsWith(".json"));
36
82
  } catch {
37
83
  return false;
38
84
  }
@@ -45,16 +91,25 @@ function ciDetect(home, want) {
45
91
  var CREDENTIALS = [
46
92
  {
47
93
  id: "mimiseed",
48
- label: "Mimi Seed \uACC4\uC815 (\uD074\uB77C\uC6B0\uB4DC)",
94
+ label: {
95
+ ko: "Mimi Seed \uACC4\uC815 (\uD074\uB77C\uC6B0\uB4DC)",
96
+ en: "Mimi Seed account (cloud)"
97
+ },
49
98
  requirement: "required",
50
99
  group: "core",
51
100
  file: "~/.mimi-seed/config.json",
52
101
  fix: "mimi-seed init",
53
102
  setup: { kind: "command", run: "mimi-seed init" },
54
- obtain: [
55
- "https://mimi-seed.pryzm.gg \uC5D0\uC11C \uAC00\uC785/\uB85C\uADF8\uC778\uD558\uBA74 \uB05D \u2014 \uBE0C\uB77C\uC6B0\uC800\uAC00 \uC54C\uC544\uC11C \uD1A0\uD070\uC744 \uB118\uACA8\uC900\uB2E4.",
56
- "CI \uC5D0\uC11C\uB294 MIMI_SEED_TOKEN \uD658\uACBD\uBCC0\uC218\uB85C \uB300\uCCB4\uD560 \uC218 \uC788\uB2E4."
57
- ],
103
+ obtain: {
104
+ ko: [
105
+ "https://mimi-seed.pryzm.gg \uC5D0\uC11C \uAC00\uC785/\uB85C\uADF8\uC778\uD558\uBA74 \uB05D \u2014 \uBE0C\uB77C\uC6B0\uC800\uAC00 \uC54C\uC544\uC11C \uD1A0\uD070\uC744 \uB118\uACA8\uC900\uB2E4.",
106
+ "CI \uC5D0\uC11C\uB294 MIMI_SEED_TOKEN \uD658\uACBD\uBCC0\uC218\uB85C \uB300\uCCB4\uD560 \uC218 \uC788\uB2E4."
107
+ ],
108
+ en: [
109
+ "Sign up / sign in at https://mimi-seed.pryzm.gg \u2014 the browser hands the token back for you.",
110
+ "In CI, set the MIMI_SEED_TOKEN environment variable instead."
111
+ ]
112
+ },
58
113
  docsAnchor: "cloud-pat",
59
114
  detect: (home) => {
60
115
  if (process.env.MIMI_SEED_TOKEN) return { present: true, detail: "MIMI_SEED_TOKEN (env)" };
@@ -64,37 +119,62 @@ var CREDENTIALS = [
64
119
  },
65
120
  {
66
121
  id: "oauth",
67
- label: "Google OAuth",
122
+ label: {
123
+ ko: "Google OAuth",
124
+ en: "Google OAuth"
125
+ },
68
126
  requirement: "required",
69
127
  group: "core",
70
128
  file: "~/.mimi-seed/tokens.json",
71
129
  fix: "mimi-seed auth login",
72
130
  setup: { kind: "mcp-bin", bin: "mimi-seed-auth" },
73
- obtain: [
74
- "\uBBF8\uB9AC \uBC1C\uAE09\uBC1B\uC744 \uAC8C **\uC5C6\uB2E4**. \uBE0C\uB77C\uC6B0\uC800 \uB85C\uADF8\uC778 \uD55C \uBC88\uC774\uBA74 \uB05D.",
75
- "\uC774 \uD1A0\uD070 \uD558\uB098\uAC00 Firebase \xB7 AdMob \xB7 Play \xB7 Google Ads \xB7 Search Console \xB7 GA4 \xB7 IAM \xB7 BigQuery \uB97C \uBAA8\uB450 \uC5F0\uB2E4.",
76
- "\u26A0\uFE0F \uB85C\uADF8\uC778 \uD654\uBA74\uC5D0\uC11C \uAC70\uBD80(access_denied) \uB2F9\uD558\uBA74 \uC571\uC774 \uC544\uC9C1 '\uD14C\uC2A4\uD305' \uBAA8\uB4DC\uB77C \uADF8\uB807\uB2E4 \u2014",
77
- " \uC6B4\uC601\uC790\uC5D0\uAC8C \uB124 Google \uACC4\uC815\uC744 Test users \uC5D0 \uCD94\uAC00\uD574\uB2EC\uB77C\uACE0 \uC694\uCCAD\uD574\uC57C \uD55C\uB2E4."
78
- ],
131
+ obtain: {
132
+ ko: [
133
+ "\uBBF8\uB9AC \uBC1C\uAE09\uBC1B\uC744 \uAC8C **\uC5C6\uB2E4**. \uBE0C\uB77C\uC6B0\uC800 \uB85C\uADF8\uC778 \uD55C \uBC88\uC774\uBA74 \uB05D.",
134
+ "\uC774 \uD1A0\uD070 \uD558\uB098\uAC00 Firebase \xB7 AdMob \xB7 Play \xB7 Google Ads \xB7 Search Console \xB7 GA4 \xB7 IAM \xB7 BigQuery \uB97C \uBAA8\uB450 \uC5F0\uB2E4.",
135
+ "\u26A0\uFE0F \uB85C\uADF8\uC778 \uD654\uBA74\uC5D0\uC11C \uAC70\uBD80(access_denied) \uB2F9\uD558\uBA74 \uC571\uC774 \uC544\uC9C1 '\uD14C\uC2A4\uD305' \uBAA8\uB4DC\uB77C \uADF8\uB807\uB2E4 \u2014",
136
+ " \uC6B4\uC601\uC790\uC5D0\uAC8C \uB124 Google \uACC4\uC815\uC744 Test users \uC5D0 \uCD94\uAC00\uD574\uB2EC\uB77C\uACE0 \uC694\uCCAD\uD574\uC57C \uD55C\uB2E4."
137
+ ],
138
+ en: [
139
+ "**Nothing** to fetch beforehand. One browser sign-in is all it takes.",
140
+ "This single token unlocks Firebase \xB7 AdMob \xB7 Play \xB7 Google Ads \xB7 Search Console \xB7 GA4 \xB7 IAM \xB7 BigQuery.",
141
+ "\u26A0\uFE0F If the login screen blocks you (access_denied), the app is still in 'testing' mode \u2014",
142
+ " ask whoever operates it to add your Google account under Test users. Retrying alone never works."
143
+ ]
144
+ },
79
145
  docsAnchor: "google-oauth",
80
- note: "8\uAC1C Google \uC11C\uBE44\uC2A4\uC758 \uACF5\uD1B5 \uAD00\uBB38",
146
+ note: {
147
+ ko: "8\uAC1C Google \uC11C\uBE44\uC2A4\uC758 \uACF5\uD1B5 \uAD00\uBB38",
148
+ en: "the shared gateway to 8 Google services"
149
+ },
81
150
  detect: (home) => ({ present: hasFile(home, "tokens.json") })
82
151
  },
83
152
  {
84
153
  id: "appstore",
85
- label: "App Store Connect",
154
+ label: {
155
+ ko: "App Store Connect",
156
+ en: "App Store Connect"
157
+ },
86
158
  requirement: "platform",
87
159
  platform: "ios",
88
160
  group: "core",
89
161
  file: "~/.mimi-seed/appstore.json",
90
162
  fix: "mimi-seed auth appstore",
91
163
  setup: { kind: "mcp-bin", bin: "mimi-seed-appstore-auth" },
92
- obtain: [
93
- "\uC720\uB8CC Apple Developer Program \uBA64\uBC84\uC2ED + Admin/App Manager \uAD8C\uD55C\uC774 \uBA3C\uC800 \uD544\uC694\uD558\uB2E4.",
94
- "App Store Connect \u2192 \uC0AC\uC6A9\uC790 \uBC0F \uC561\uC138\uC2A4 \u2192 \uD1B5\uD569 \u2192 App Store Connect API \u2192 \uD0A4 \uC0DD\uC131",
95
- "\uAC00\uC838\uC62C \uAC83 3\uAC1C: Issuer ID \xB7 Key ID \xB7 .p8 \uD30C\uC77C",
96
- "\u26A0\uFE0F .p8 \uC740 **\uB531 \uD55C \uBC88\uB9CC** \uB2E4\uC6B4\uB85C\uB4DC\uB41C\uB2E4. \uC783\uC5B4\uBC84\uB9AC\uBA74 \uD0A4\uB97C \uD3D0\uAE30\uD558\uACE0 \uC0C8\uB85C \uB9CC\uB4E4\uC5B4\uC57C \uD55C\uB2E4."
97
- ],
164
+ obtain: {
165
+ ko: [
166
+ "\uC720\uB8CC Apple Developer Program \uBA64\uBC84\uC2ED + Admin/App Manager \uAD8C\uD55C\uC774 \uBA3C\uC800 \uD544\uC694\uD558\uB2E4.",
167
+ "App Store Connect \u2192 \uC0AC\uC6A9\uC790 \uBC0F \uC561\uC138\uC2A4 \u2192 \uD1B5\uD569 \u2192 App Store Connect API \u2192 \uD0A4 \uC0DD\uC131",
168
+ "\uAC00\uC838\uC62C \uAC83 3\uAC1C: Issuer ID \xB7 Key ID \xB7 .p8 \uD30C\uC77C",
169
+ "\u26A0\uFE0F .p8 \uC740 **\uB531 \uD55C \uBC88\uB9CC** \uB2E4\uC6B4\uB85C\uB4DC\uB41C\uB2E4. \uC783\uC5B4\uBC84\uB9AC\uBA74 \uD0A4\uB97C \uD3D0\uAE30\uD558\uACE0 \uC0C8\uB85C \uB9CC\uB4E4\uC5B4\uC57C \uD55C\uB2E4."
170
+ ],
171
+ en: [
172
+ "You need a **paid** Apple Developer Program membership and the Admin / App Manager role first.",
173
+ "App Store Connect \u2192 Users and Access \u2192 Integrations \u2192 App Store Connect API \u2192 generate a key",
174
+ "Collect three things: Issuer ID \xB7 Key ID \xB7 the .p8 file",
175
+ "\u26A0\uFE0F The .p8 downloads **exactly once**. Lose it and you must revoke the key and issue a new one."
176
+ ]
177
+ },
98
178
  docsAnchor: "app-store-connect",
99
179
  detect: (home) => {
100
180
  const cfg = readJson(home, "appstore.json");
@@ -103,7 +183,10 @@ var CREDENTIALS = [
103
183
  },
104
184
  {
105
185
  id: "playstore",
106
- label: "Play \uC11C\uBE44\uC2A4 \uACC4\uC815",
186
+ label: {
187
+ ko: "Play \uC11C\uBE44\uC2A4 \uACC4\uC815",
188
+ en: "Play service account"
189
+ },
107
190
  // 선택이다 — helpers.ts 의 requirePlayStoreAuth 가 SA → OAuth 로 폴백하고,
108
191
  // 로그인 시 androidpublisher 스코프를 이미 받으므로 로컬 작업은 OAuth 로 된다.
109
192
  requirement: "optional",
@@ -112,83 +195,141 @@ var CREDENTIALS = [
112
195
  fallbackOn: ["oauth"],
113
196
  fix: "mimi-seed auth playstore",
114
197
  setup: { kind: "mcp-bin", bin: "mimi-seed-playstore-auth" },
115
- obtain: [
116
- "\uB85C\uCEEC\uC5D0\uC11C \uC4F8 \uAC70\uBA74 **\uD544\uC694 \uC5C6\uB2E4** \u2014 Google OAuth \uB85C \uB300\uBD80\uBD84\uC758 Play \uC791\uC5C5\uC774 \uB41C\uB2E4.",
117
- "CI/\uD5E4\uB4DC\uB9AC\uC2A4(\uBE0C\uB77C\uC6B0\uC800 \uC5C6\uB294 \uD658\uACBD)\uC5D0\uC11C\uB9CC \uD544\uC694:",
118
- " 1. GCP Console \u2192 IAM & Admin \u2192 \uC11C\uBE44\uC2A4 \uACC4\uC815 \u2192 \uD0A4 \u2192 \uC0C8 \uD0A4 \uB9CC\uB4E4\uAE30 \u2192 JSON",
119
- " 2. Play Console \u2192 \uC124\uC815 \u2192 \uC0AC\uC6A9\uC790 \uBC0F \uAD8C\uD55C \u2192 \uADF8 \uC11C\uBE44\uC2A4 \uACC4\uC815 \uC774\uBA54\uC77C\uC744 \uCD08\uB300",
120
- " 3. \uAD8C\uD55C \uC804\uD30C\uC5D0 ~5\uBD84 \uAC78\uB9B0\uB2E4. \uADF8 \uC804\uC5D4 403 \uC774 \uB728\uB294 \uAC8C \uC815\uC0C1\uC774\uB2E4.",
121
- "Claude \uC5D0\uAC8C `setup_playstore_connection` \uC744 \uC2DC\uD0A4\uBA74 1\uBC88\uC744 \uC790\uB3D9\uD654\uD574\uC900\uB2E4."
122
- ],
198
+ obtain: {
199
+ ko: [
200
+ "\uB85C\uCEEC\uC5D0\uC11C \uC4F8 \uAC70\uBA74 **\uD544\uC694 \uC5C6\uB2E4** \u2014 Google OAuth \uB85C \uB300\uBD80\uBD84\uC758 Play \uC791\uC5C5\uC774 \uB41C\uB2E4.",
201
+ "CI/\uD5E4\uB4DC\uB9AC\uC2A4(\uBE0C\uB77C\uC6B0\uC800 \uC5C6\uB294 \uD658\uACBD)\uC5D0\uC11C\uB9CC \uD544\uC694:",
202
+ " 1. GCP Console \u2192 IAM & Admin \u2192 \uC11C\uBE44\uC2A4 \uACC4\uC815 \u2192 \uD0A4 \u2192 \uC0C8 \uD0A4 \uB9CC\uB4E4\uAE30 \u2192 JSON",
203
+ " 2. Play Console \u2192 \uC124\uC815 \u2192 \uC0AC\uC6A9\uC790 \uBC0F \uAD8C\uD55C \u2192 \uADF8 \uC11C\uBE44\uC2A4 \uACC4\uC815 \uC774\uBA54\uC77C\uC744 \uCD08\uB300",
204
+ " 3. \uAD8C\uD55C \uC804\uD30C\uC5D0 ~5\uBD84 \uAC78\uB9B0\uB2E4. \uADF8 \uC804\uC5D4 403 \uC774 \uB728\uB294 \uAC8C \uC815\uC0C1\uC774\uB2E4.",
205
+ "Claude \uC5D0\uAC8C `setup_playstore_connection` \uC744 \uC2DC\uD0A4\uBA74 1\uBC88\uC744 \uC790\uB3D9\uD654\uD574\uC900\uB2E4."
206
+ ],
207
+ en: [
208
+ "Working locally? You **don't need this** \u2014 your Google OAuth login covers most Play work.",
209
+ "Only needed in CI / headless environments (no browser):",
210
+ " 1. GCP Console \u2192 IAM & Admin \u2192 Service Accounts \u2192 Keys \u2192 Add key \u2192 Create new key \u2192 JSON",
211
+ " 2. Play Console \u2192 Settings \u2192 Users and permissions \u2192 invite that service account's email",
212
+ " 3. The grant takes ~5 min to propagate. Until then a 403 is expected, not a bug.",
213
+ "Ask Claude to run `setup_playstore_connection` and step 1 is done for you."
214
+ ]
215
+ },
123
216
  docsAnchor: "play-service-account",
124
- note: "\uC120\uD0DD \u2014 \uB85C\uCEEC\uC740 OAuth \uB85C \uAC00\uB2A5, CI/\uD5E4\uB4DC\uB9AC\uC2A4\uB294 \uD544\uC218",
217
+ note: {
218
+ ko: "\uC120\uD0DD \u2014 \uB85C\uCEEC\uC740 OAuth \uB85C \uAC00\uB2A5, CI/\uD5E4\uB4DC\uB9AC\uC2A4\uB294 \uD544\uC218",
219
+ en: "optional \u2014 OAuth covers local; required for CI / headless"
220
+ },
125
221
  detect: (home) => ({ present: hasPlaySa(home) })
126
222
  },
127
223
  {
128
224
  id: "bigquery",
129
- label: "BigQuery \uC11C\uBE44\uC2A4 \uACC4\uC815",
225
+ label: {
226
+ ko: "BigQuery \uC11C\uBE44\uC2A4 \uACC4\uC815",
227
+ en: "BigQuery service account"
228
+ },
130
229
  requirement: "optional",
131
230
  group: "core",
132
231
  file: "~/.mimi-seed/bigquery-service-account.json",
133
232
  fallbackOn: ["oauth"],
134
233
  fix: "mimi-seed auth bigquery",
135
234
  setup: { kind: "mcp-bin", bin: "mimi-seed-bigquery-auth" },
136
- obtain: [
137
- "OAuth \uB85C\uB3C4 \uB3D9\uC791\uD558\uC9C0\uB9CC, Workspace \uC7AC\uC778\uC99D \uC815\uCC45(invalid_rapt)\uC5D0 \uB9C9\uD788\uB294 \uD658\uACBD\uC774\uBA74 \uC11C\uBE44\uC2A4 \uACC4\uC815\uC774 \uD544\uC694\uD558\uB2E4.",
138
- "GCP Console \u2192 IAM & Admin \u2192 \uC11C\uBE44\uC2A4 \uACC4\uC815 \u2192 \uD0A4 \u2192 \uC0C8 \uD0A4 \uB9CC\uB4E4\uAE30 \u2192 JSON",
139
- "\uADF8 \uC11C\uBE44\uC2A4 \uACC4\uC815\uC5D0 IAM \uC5ED\uD560\uC744 **\uC9C1\uC811** \uBD80\uC5EC\uD574\uC57C \uD55C\uB2E4 (\uC790\uB3D9\uC73C\uB85C \uC548 \uB41C\uB2E4):",
140
- " \u2022 roles/bigquery.jobUser (\uCFFC\uB9AC \uC2E4\uD589)",
141
- " \u2022 roles/bigquery.dataViewer (\uB370\uC774\uD130\uC14B \uC77D\uAE30)"
142
- ],
235
+ obtain: {
236
+ ko: [
237
+ "OAuth \uB85C\uB3C4 \uB3D9\uC791\uD558\uC9C0\uB9CC, Workspace \uC7AC\uC778\uC99D \uC815\uCC45(invalid_rapt)\uC5D0 \uB9C9\uD788\uB294 \uD658\uACBD\uC774\uBA74 \uC11C\uBE44\uC2A4 \uACC4\uC815\uC774 \uD544\uC694\uD558\uB2E4.",
238
+ "GCP Console \u2192 IAM & Admin \u2192 \uC11C\uBE44\uC2A4 \uACC4\uC815 \u2192 \uD0A4 \u2192 \uC0C8 \uD0A4 \uB9CC\uB4E4\uAE30 \u2192 JSON",
239
+ "\uADF8 \uC11C\uBE44\uC2A4 \uACC4\uC815\uC5D0 IAM \uC5ED\uD560\uC744 **\uC9C1\uC811** \uBD80\uC5EC\uD574\uC57C \uD55C\uB2E4 (\uC790\uB3D9\uC73C\uB85C \uC548 \uB41C\uB2E4):",
240
+ " \u2022 roles/bigquery.jobUser (\uCFFC\uB9AC \uC2E4\uD589)",
241
+ " \u2022 roles/bigquery.dataViewer (\uB370\uC774\uD130\uC14B \uC77D\uAE30)"
242
+ ],
243
+ en: [
244
+ "OAuth works too \u2014 a service account only matters if Workspace reauth policy (invalid_rapt) keeps blocking you.",
245
+ "GCP Console \u2192 IAM & Admin \u2192 Service Accounts \u2192 Keys \u2192 Add key \u2192 Create new key \u2192 JSON",
246
+ "You must grant that service account these IAM roles **yourself** (nothing does it for you):",
247
+ " \u2022 roles/bigquery.jobUser (run queries)",
248
+ " \u2022 roles/bigquery.dataViewer (read datasets)"
249
+ ]
250
+ },
143
251
  docsAnchor: "bigquery",
144
- note: "\uC120\uD0DD \u2014 OAuth \uD3F4\uBC31 \uAC00\uB2A5",
252
+ note: {
253
+ ko: "\uC120\uD0DD \u2014 OAuth \uD3F4\uBC31 \uAC00\uB2A5",
254
+ en: "optional \u2014 falls back to OAuth"
255
+ },
145
256
  detect: (home) => ({ present: anyFileStarting(home, "bigquery") })
146
257
  },
147
258
  {
148
259
  id: "github",
149
- label: "GitHub Actions",
260
+ label: {
261
+ ko: "GitHub Actions",
262
+ en: "GitHub Actions"
263
+ },
150
264
  requirement: "optional",
151
265
  group: "ci",
152
266
  file: "~/.mimi-seed/ci.json",
153
267
  fix: "mimi-seed auth ci",
154
268
  setup: { kind: "cli", handler: "github" },
155
- obtain: [
156
- "GitHub \u2192 Settings \u2192 Developer settings \u2192 Personal access tokens",
157
- "\u26A0\uFE0F \uC2A4\uCF54\uD504 **`repo` \uC640 `workflow` \uB97C \uB458 \uB2E4** \uCCB4\uD06C\uD574\uC57C \uD55C\uB2E4.",
158
- " `workflow` \uAC00 \uC5C6\uC73C\uBA74 \uC6CC\uD06C\uD50C\uB85C dispatch \uAC00 403 \uC73C\uB85C \uB9C9\uD78C\uB2E4.",
159
- "\uADF8 \uC678: owner(\uC870\uC9C1/\uC0AC\uC6A9\uC790) \xB7 repo \uC774\uB984. GitHub Enterprise \uBA74 host URL \uB3C4."
160
- ],
269
+ obtain: {
270
+ ko: [
271
+ "GitHub \u2192 Settings \u2192 Developer settings \u2192 Personal access tokens",
272
+ "\u26A0\uFE0F \uC2A4\uCF54\uD504 **`repo` \uC640 `workflow` \uB97C \uB458 \uB2E4** \uCCB4\uD06C\uD574\uC57C \uD55C\uB2E4.",
273
+ " `workflow` \uAC00 \uC5C6\uC73C\uBA74 \uC6CC\uD06C\uD50C\uB85C dispatch \uAC00 403 \uC73C\uB85C \uB9C9\uD78C\uB2E4.",
274
+ "\uADF8 \uC678: owner(\uC870\uC9C1/\uC0AC\uC6A9\uC790) \xB7 repo \uC774\uB984. GitHub Enterprise \uBA74 host URL \uB3C4."
275
+ ],
276
+ en: [
277
+ "GitHub \u2192 Settings \u2192 Developer settings \u2192 Personal access tokens",
278
+ "\u26A0\uFE0F Check **both the `repo` and `workflow`** scopes.",
279
+ " Without `workflow`, dispatching a workflow fails with 403 (reading still works).",
280
+ "Also: owner (org/user) \xB7 repo name. Plus the host URL on GitHub Enterprise."
281
+ ]
282
+ },
161
283
  docsAnchor: "ci-github-gitlab",
162
284
  detect: (home) => ciDetect(home, "github")
163
285
  },
164
286
  {
165
287
  id: "gitlab",
166
- label: "GitLab CI",
288
+ label: {
289
+ ko: "GitLab CI",
290
+ en: "GitLab CI"
291
+ },
167
292
  requirement: "optional",
168
293
  group: "ci",
169
294
  file: "~/.mimi-seed/ci.json",
170
295
  fix: "mimi-seed auth ci",
171
296
  setup: { kind: "cli", handler: "gitlab" },
172
- obtain: [
173
- "GitLab \u2192 \uC0AC\uC6A9\uC790 \uC124\uC815 \u2192 Access Tokens \u2192 `api` \uC2A4\uCF54\uD504\uB85C \uBC1C\uAE09 (glpat-\u2026)",
174
- "\uADF8 \uC678: namespace/group \xB7 \uD504\uB85C\uC81D\uD2B8 \uC774\uB984. self-hosted \uBA74 URL \uB3C4."
175
- ],
297
+ obtain: {
298
+ ko: [
299
+ "GitLab \u2192 \uC0AC\uC6A9\uC790 \uC124\uC815 \u2192 Access Tokens \u2192 `api` \uC2A4\uCF54\uD504\uB85C \uBC1C\uAE09 (glpat-\u2026)",
300
+ "\uADF8 \uC678: namespace/group \xB7 \uD504\uB85C\uC81D\uD2B8 \uC774\uB984. self-hosted \uBA74 URL \uB3C4."
301
+ ],
302
+ en: [
303
+ "GitLab \u2192 User settings \u2192 Access tokens \u2192 issue one with the `api` scope (glpat-\u2026)",
304
+ "Also: namespace/group \xB7 project name. Plus the URL if self-hosted."
305
+ ]
306
+ },
176
307
  docsAnchor: "ci-github-gitlab",
177
308
  detect: (home) => ciDetect(home, "gitlab")
178
309
  },
179
310
  {
180
311
  id: "jenkins",
181
- label: "Jenkins",
312
+ label: {
313
+ ko: "Jenkins",
314
+ en: "Jenkins"
315
+ },
182
316
  requirement: "optional",
183
317
  group: "ci",
184
318
  file: "~/.mimi-seed/jenkins.json",
185
319
  fix: "mimi-seed auth jenkins",
186
320
  setup: { kind: "mcp-bin", bin: "mimi-seed-jenkins-auth" },
187
- obtain: [
188
- 'Jenkins \u2192 [\uC0AC\uC6A9\uC790 \uC774\uB984] \u2192 \uC124\uC815 \u2192 API Token \u2192 "Add new Token"',
189
- "\uBE44\uBC00\uBC88\uD638\uAC00 \uC544\uB2C8\uB77C **API Token** \uC774\uB2E4.",
190
- "\uB85C\uCEEC Jenkins \uAC00 \uC5C6\uC5B4\uB3C4 \uD68C\uC0AC/\uC6D0\uACA9 \uC11C\uBC84 URL \uC744 \uADF8\uB300\uB85C \uC4F0\uBA74 \uB41C\uB2E4."
191
- ],
321
+ obtain: {
322
+ ko: [
323
+ 'Jenkins \u2192 [\uC0AC\uC6A9\uC790 \uC774\uB984] \u2192 \uC124\uC815 \u2192 API Token \u2192 "Add new Token"',
324
+ "\uBE44\uBC00\uBC88\uD638\uAC00 \uC544\uB2C8\uB77C **API Token** \uC774\uB2E4.",
325
+ "\uB85C\uCEEC Jenkins \uAC00 \uC5C6\uC5B4\uB3C4 \uD68C\uC0AC/\uC6D0\uACA9 \uC11C\uBC84 URL \uC744 \uADF8\uB300\uB85C \uC4F0\uBA74 \uB41C\uB2E4."
326
+ ],
327
+ en: [
328
+ 'Jenkins \u2192 [your name] \u2192 Configure \u2192 API Token \u2192 "Add new Token"',
329
+ "That is an **API token**, not your password.",
330
+ "No local Jenkins needed \u2014 a company or remote server URL works just as well."
331
+ ]
332
+ },
192
333
  docsAnchor: "jenkins",
193
334
  detect: (home) => {
194
335
  const cfg = readJson(home, "jenkins.json");
@@ -197,19 +338,31 @@ var CREDENTIALS = [
197
338
  },
198
339
  {
199
340
  id: "googleads",
200
- label: "Google Ads",
341
+ label: {
342
+ ko: "Google Ads",
343
+ en: "Google Ads"
344
+ },
201
345
  requirement: "optional",
202
346
  group: "marketing",
203
347
  file: "~/.mimi-seed/google-ads.json",
204
348
  fix: "mimi-seed auth googleads",
205
349
  setup: { kind: "mcp-bin", bin: "mimi-seed-googleads-auth" },
206
- obtain: [
207
- "Google Ads \u2192 \uB3C4\uAD6C \uBC0F \uC124\uC815 \u2192 \uC124\uC815 \u2192 API \uC13C\uD130 \u2192 Developer Token \uBC1C\uAE09",
208
- "\u26A0\uFE0F \uCD5C\uCD08 \uBC1C\uAE09 \uD1A0\uD070\uC740 '\uD14C\uC2A4\uD2B8' \uB4F1\uAE09\uC774\uB2E4. \uC2E4\uACC4\uC815 \uB370\uC774\uD130\uB97C \uBCF4\uB824\uBA74 **\uC2B9\uC778 \uC2EC\uC0AC**\uB97C \uD1B5\uACFC\uD574\uC57C \uD558\uACE0,",
209
- " \uC2EC\uC0AC\uC5D0\uB294 \uC2DC\uAC04\uC774 \uAC78\uB9B0\uB2E4 (\uC989\uC2DC \uBC1C\uAE09\uB418\uC9C0 \uC54A\uB294\uB2E4).",
210
- "\uADF8 \uC678: Customer ID (\uC608: 123-456-7890). MCC \uB97C \uC4F0\uBA74 \uAD00\uB9AC\uC790 \uACC4\uC815 ID \uB3C4.",
211
- "\uC778\uC99D \uC790\uCCB4\uB294 Google OAuth \uC758 `adwords` \uC2A4\uCF54\uD504\uB97C \uD0C4\uB2E4 \u2014 \uC61B \uD1A0\uD070\uC774\uBA74 \uC7AC\uB85C\uADF8\uC778\uC774 \uD544\uC694\uD560 \uC218 \uC788\uB2E4."
212
- ],
350
+ obtain: {
351
+ ko: [
352
+ "Google Ads \u2192 \uB3C4\uAD6C \uBC0F \uC124\uC815 \u2192 \uC124\uC815 \u2192 API \uC13C\uD130 \u2192 Developer Token \uBC1C\uAE09",
353
+ "\u26A0\uFE0F \uCD5C\uCD08 \uBC1C\uAE09 \uD1A0\uD070\uC740 '\uD14C\uC2A4\uD2B8' \uB4F1\uAE09\uC774\uB2E4. \uC2E4\uACC4\uC815 \uB370\uC774\uD130\uB97C \uBCF4\uB824\uBA74 **\uC2B9\uC778 \uC2EC\uC0AC**\uB97C \uD1B5\uACFC\uD574\uC57C \uD558\uACE0,",
354
+ " \uC2EC\uC0AC\uC5D0\uB294 \uC2DC\uAC04\uC774 \uAC78\uB9B0\uB2E4 (\uC989\uC2DC \uBC1C\uAE09\uB418\uC9C0 \uC54A\uB294\uB2E4).",
355
+ "\uADF8 \uC678: Customer ID (\uC608: 123-456-7890). MCC \uB97C \uC4F0\uBA74 \uAD00\uB9AC\uC790 \uACC4\uC815 ID \uB3C4.",
356
+ "\uC778\uC99D \uC790\uCCB4\uB294 Google OAuth \uC758 `adwords` \uC2A4\uCF54\uD504\uB97C \uD0C4\uB2E4 \u2014 \uC61B \uD1A0\uD070\uC774\uBA74 \uC7AC\uB85C\uADF8\uC778\uC774 \uD544\uC694\uD560 \uC218 \uC788\uB2E4."
357
+ ],
358
+ en: [
359
+ "Google Ads \u2192 Tools and settings \u2192 Setup \u2192 API Center \u2192 apply for a developer token",
360
+ "\u26A0\uFE0F The token you get immediately is 'test' tier. Reaching real account data requires Google to **approve**",
361
+ " your application, and that takes time (it is not instant).",
362
+ "Also: your Customer ID (e.g. 123-456-7890), plus the manager (MCC) account ID if you use one.",
363
+ "Auth itself rides on Google OAuth's `adwords` scope \u2014 an older token may need a re-login."
364
+ ]
365
+ },
213
366
  docsAnchor: "google-ads",
214
367
  detect: (home) => {
215
368
  const cfg = readJson(home, "google-ads.json");
@@ -218,18 +371,29 @@ var CREDENTIALS = [
218
371
  },
219
372
  {
220
373
  id: "facebook",
221
- label: "Facebook \uD398\uC774\uC9C0",
374
+ label: {
375
+ ko: "Facebook \uD398\uC774\uC9C0",
376
+ en: "Facebook Page"
377
+ },
222
378
  requirement: "optional",
223
379
  group: "marketing",
224
380
  file: "~/.mimi-seed/facebook.json",
225
381
  fix: "mimi-seed auth facebook",
226
382
  setup: { kind: "mcp-bin", bin: "mimi-seed-social-auth", args: ["facebook"] },
227
- obtain: [
228
- "Meta \uAC1C\uBC1C\uC790 \uC571 + \uB300\uC0C1 \uD398\uC774\uC9C0\uC758 \uAD00\uB9AC\uC790 \uAD8C\uD55C\uC774 \uBA3C\uC800 \uD544\uC694\uD558\uB2E4.",
229
- "Graph API Explorer \u2192 \uAD8C\uD55C pages_show_list, pages_manage_posts, pages_read_engagement",
230
- "\u2192 User Token \uC0DD\uC131 \u2192 /me/accounts \uD638\uCD9C \u2192 \uADF8 \uD398\uC774\uC9C0\uC758 access_token (EAA\u2026)",
231
- "long-lived \uD1A0\uD070\uC744 \uAD8C\uC7A5\uD55C\uB2E4 (\uC57D 60\uC77C)."
232
- ],
383
+ obtain: {
384
+ ko: [
385
+ "Meta \uAC1C\uBC1C\uC790 \uC571 + \uB300\uC0C1 \uD398\uC774\uC9C0\uC758 \uAD00\uB9AC\uC790 \uAD8C\uD55C\uC774 \uBA3C\uC800 \uD544\uC694\uD558\uB2E4.",
386
+ "Graph API Explorer \u2192 \uAD8C\uD55C pages_show_list, pages_manage_posts, pages_read_engagement",
387
+ "\u2192 User Token \uC0DD\uC131 \u2192 /me/accounts \uD638\uCD9C \u2192 \uADF8 \uD398\uC774\uC9C0\uC758 access_token (EAA\u2026)",
388
+ "long-lived \uD1A0\uD070\uC744 \uAD8C\uC7A5\uD55C\uB2E4 (\uC57D 60\uC77C)."
389
+ ],
390
+ en: [
391
+ "You need a Meta developer app and admin rights on the target Page first.",
392
+ "Graph API Explorer \u2192 permissions pages_show_list, pages_manage_posts, pages_read_engagement",
393
+ "\u2192 generate a User token \u2192 call /me/accounts \u2192 copy that Page's access_token (EAA\u2026)",
394
+ "Prefer a long-lived token (~60 days)."
395
+ ]
396
+ },
233
397
  docsAnchor: "facebook",
234
398
  detect: (home) => {
235
399
  const cfg = readJson(home, "facebook.json");
@@ -238,18 +402,29 @@ var CREDENTIALS = [
238
402
  },
239
403
  {
240
404
  id: "instagram",
241
- label: "Instagram",
405
+ label: {
406
+ ko: "Instagram",
407
+ en: "Instagram"
408
+ },
242
409
  requirement: "optional",
243
410
  group: "marketing",
244
411
  file: "~/.mimi-seed/instagram.json",
245
412
  fix: "mimi-seed auth instagram",
246
413
  setup: { kind: "mcp-bin", bin: "mimi-seed-social-auth", args: ["instagram"] },
247
- obtain: [
248
- "long-lived \uD1A0\uD070 \uB450 \uD615\uC2DD \uBAA8\uB450 \uC9C0\uC6D0\uD55C\uB2E4 (\uC790\uB3D9 \uAC10\uC9C0):",
249
- " \u2022 IGAA\u2026 \u2014 Instagram Login (Meta \uC2E0\uADDC \uBC29\uC2DD, Facebook \uD398\uC774\uC9C0 \uBD88\uD544\uC694)",
250
- " \u2022 EAA\u2026 \u2014 Facebook Login (IG **\uBE44\uC988\uB2C8\uC2A4** \uACC4\uC815\uC774 FB \uD398\uC774\uC9C0\uC5D0 \uC5F0\uACB0\uB3FC \uC788\uC5B4\uC57C \uD568)",
251
- "\uD1A0\uD070 \uC218\uBA85\uC740 \uC57D 60\uC77C\uC774\uB2E4."
252
- ],
414
+ obtain: {
415
+ ko: [
416
+ "long-lived \uD1A0\uD070 \uB450 \uD615\uC2DD \uBAA8\uB450 \uC9C0\uC6D0\uD55C\uB2E4 (\uC790\uB3D9 \uAC10\uC9C0):",
417
+ " \u2022 IGAA\u2026 \u2014 Instagram Login (Meta \uC2E0\uADDC \uBC29\uC2DD, Facebook \uD398\uC774\uC9C0 \uBD88\uD544\uC694)",
418
+ " \u2022 EAA\u2026 \u2014 Facebook Login (IG **\uBE44\uC988\uB2C8\uC2A4** \uACC4\uC815\uC774 FB \uD398\uC774\uC9C0\uC5D0 \uC5F0\uACB0\uB3FC \uC788\uC5B4\uC57C \uD568)",
419
+ "\uD1A0\uD070 \uC218\uBA85\uC740 \uC57D 60\uC77C\uC774\uB2E4."
420
+ ],
421
+ en: [
422
+ "Both long-lived token shapes are supported (auto-detected):",
423
+ " \u2022 IGAA\u2026 \u2014 Instagram Login (Meta's newer path, no Facebook Page required)",
424
+ " \u2022 EAA\u2026 \u2014 Facebook Login (needs an Instagram **Business** account linked to a FB Page)",
425
+ "Tokens last about 60 days."
426
+ ]
427
+ },
253
428
  docsAnchor: "instagram",
254
429
  detect: (home) => {
255
430
  const cfg = readJson(home, "instagram.json");
@@ -258,25 +433,38 @@ var CREDENTIALS = [
258
433
  },
259
434
  {
260
435
  id: "anthropic",
261
- label: "ANTHROPIC_API_KEY",
436
+ label: {
437
+ ko: "ANTHROPIC_API_KEY",
438
+ en: "ANTHROPIC_API_KEY"
439
+ },
262
440
  requirement: "optional",
263
441
  group: "marketing",
264
442
  fix: "export ANTHROPIC_API_KEY=sk-ant-\u2026",
265
443
  setup: { kind: "env", envVar: "ANTHROPIC_API_KEY" },
266
- obtain: [
267
- "Anthropic Console (console.anthropic.com) \u2192 API Keys \uC5D0\uC11C \uBC1C\uAE09.",
268
- "\uD658\uACBD\uBCC0\uC218\uB85C\uB9CC \uC77D\uB294\uB2E4 \u2014 \uC124\uC815 \uBA85\uB839\uC774 \uB530\uB85C \uC5C6\uB2E4.",
269
- "\uC5C6\uC5B4\uB3C4 \uB41C\uB2E4: `mimi-seed notes` \uB294 AI \uC5C6\uC774 \uCEE4\uBC0B \uBAA9\uB85D \uAE30\uBC18\uC73C\uB85C \uB3D9\uC791\uD55C\uB2E4."
270
- ],
444
+ obtain: {
445
+ ko: [
446
+ "Anthropic Console (console.anthropic.com) \u2192 API Keys \uC5D0\uC11C \uBC1C\uAE09.",
447
+ "\uD658\uACBD\uBCC0\uC218\uB85C\uB9CC \uC77D\uB294\uB2E4 \u2014 \uC124\uC815 \uBA85\uB839\uC774 \uB530\uB85C \uC5C6\uB2E4.",
448
+ "\uC5C6\uC5B4\uB3C4 \uB41C\uB2E4: `mimi-seed notes` \uB294 AI \uC5C6\uC774 \uCEE4\uBC0B \uBAA9\uB85D \uAE30\uBC18\uC73C\uB85C \uB3D9\uC791\uD55C\uB2E4."
449
+ ],
450
+ en: [
451
+ "Anthropic Console (console.anthropic.com) \u2192 API Keys.",
452
+ "Read from the environment only \u2014 there is no setup command.",
453
+ "Optional: without it, `mimi-seed notes` still works, formatting your commits instead of writing prose."
454
+ ]
455
+ },
271
456
  docsAnchor: "anthropic-api-key",
272
- note: "\uC120\uD0DD \u2014 AI \uB9B4\uB9AC\uC2A4 \uB178\uD2B8/\uB9AC\uBDF0 \uB2F5\uBCC0 \uC0DD\uC131\uC6A9",
457
+ note: {
458
+ ko: "\uC120\uD0DD \u2014 AI \uB9B4\uB9AC\uC2A4 \uB178\uD2B8/\uB9AC\uBDF0 \uB2F5\uBCC0 \uC0DD\uC131\uC6A9",
459
+ en: "optional \u2014 for AI-drafted release notes / review replies"
460
+ },
273
461
  detect: () => ({ present: Boolean(process.env.ANTHROPIC_API_KEY) })
274
462
  }
275
463
  ];
276
464
  function tryCredById(id) {
277
465
  return CREDENTIALS.find((c) => c.id === id);
278
466
  }
279
- function detectAll(home = os.homedir()) {
467
+ function detectAll(home = os2.homedir()) {
280
468
  return new Map(CREDENTIALS.map((c) => [c.id, c.detect(home)]));
281
469
  }
282
470
  function isSatisfied(spec, detected) {
@@ -323,7 +511,11 @@ async function runMcpBin(bin, extraArgs = []) {
323
511
  const cmd = local ? bin : "npx";
324
512
  const args = local ? extraArgs : ["-y", MCP_PKG, bin, ...extraArgs];
325
513
  return new Promise((resolve) => {
326
- const child = spawn(cmd, args, { stdio: "inherit", shell: true });
514
+ const child = spawn(cmd, args, {
515
+ stdio: "inherit",
516
+ shell: true,
517
+ env: { ...process.env, MIMI_SEED_LANG: resolveLang() }
518
+ });
327
519
  child.on("error", (e) => {
328
520
  process.stderr.write(`
329
521
  \u274C ${cmd} \uC2E4\uD589 \uC2E4\uD328: ${e.message}
@@ -335,28 +527,28 @@ async function runMcpBin(bin, extraArgs = []) {
335
527
  }
336
528
 
337
529
  // src/jenkins-config.ts
338
- import fs2 from "fs";
339
- import os2 from "os";
340
- import path2 from "path";
341
- var CONFIG_DIR = path2.join(os2.homedir(), ".mimi-seed");
342
- var JENKINS_PATH = path2.join(CONFIG_DIR, "jenkins.json");
343
- var LEGACY_PATH = path2.join(CONFIG_DIR, "config.json");
344
- function loadJenkinsConfig(home = os2.homedir()) {
530
+ import fs3 from "fs";
531
+ import os3 from "os";
532
+ import path3 from "path";
533
+ var CONFIG_DIR = path3.join(os3.homedir(), ".mimi-seed");
534
+ var JENKINS_PATH = path3.join(CONFIG_DIR, "jenkins.json");
535
+ var LEGACY_PATH = path3.join(CONFIG_DIR, "config.json");
536
+ function loadJenkinsConfig(home = os3.homedir()) {
345
537
  try {
346
- const p = path2.join(home, ".mimi-seed", "jenkins.json");
347
- return JSON.parse(fs2.readFileSync(p, "utf-8"));
538
+ const p = path3.join(home, ".mimi-seed", "jenkins.json");
539
+ return JSON.parse(fs3.readFileSync(p, "utf-8"));
348
540
  } catch {
349
541
  return null;
350
542
  }
351
543
  }
352
- function migrateLegacyJenkins(home = os2.homedir()) {
353
- const dir = path2.join(home, ".mimi-seed");
354
- const jenkinsPath = path2.join(dir, "jenkins.json");
355
- const legacyPath = path2.join(dir, "config.json");
356
- if (fs2.existsSync(jenkinsPath)) return false;
544
+ function migrateLegacyJenkins(home = os3.homedir()) {
545
+ const dir = path3.join(home, ".mimi-seed");
546
+ const jenkinsPath = path3.join(dir, "jenkins.json");
547
+ const legacyPath = path3.join(dir, "config.json");
548
+ if (fs3.existsSync(jenkinsPath)) return false;
357
549
  let legacy;
358
550
  try {
359
- legacy = JSON.parse(fs2.readFileSync(legacyPath, "utf-8"));
551
+ legacy = JSON.parse(fs3.readFileSync(legacyPath, "utf-8"));
360
552
  } catch {
361
553
  return false;
362
554
  }
@@ -370,28 +562,28 @@ function migrateLegacyJenkins(home = os2.homedir()) {
370
562
  ...j.jobAndroid ? { jobAndroid: j.jobAndroid } : {},
371
563
  ...j.jobIos ? { jobIos: j.jobIos } : {}
372
564
  };
373
- fs2.mkdirSync(dir, { recursive: true, mode: 448 });
374
- fs2.writeFileSync(jenkinsPath, JSON.stringify(migrated, null, 2), { mode: 384 });
565
+ fs3.mkdirSync(dir, { recursive: true, mode: 448 });
566
+ fs3.writeFileSync(jenkinsPath, JSON.stringify(migrated, null, 2), { mode: 384 });
375
567
  delete legacy.jenkins;
376
568
  const tmp = `${legacyPath}.tmp`;
377
- fs2.writeFileSync(tmp, JSON.stringify(legacy, null, 2), { mode: 384 });
378
- fs2.renameSync(tmp, legacyPath);
569
+ fs3.writeFileSync(tmp, JSON.stringify(legacy, null, 2), { mode: 384 });
570
+ fs3.renameSync(tmp, legacyPath);
379
571
  return true;
380
572
  }
381
573
 
382
574
  // src/detect.ts
383
- import fs3 from "fs/promises";
384
- import path3 from "path";
575
+ import fs4 from "fs/promises";
576
+ import path4 from "path";
385
577
  async function readIfExists(p) {
386
578
  try {
387
- return await fs3.readFile(p, "utf8");
579
+ return await fs4.readFile(p, "utf8");
388
580
  } catch {
389
581
  return null;
390
582
  }
391
583
  }
392
584
  async function pathExists(p) {
393
585
  try {
394
- await fs3.access(p);
586
+ await fs4.access(p);
395
587
  return true;
396
588
  } catch {
397
589
  return false;
@@ -413,16 +605,16 @@ async function walk(root, match, maxDepth = 5) {
413
605
  if (depth > maxDepth) return;
414
606
  let entries;
415
607
  try {
416
- entries = await fs3.readdir(dir, { withFileTypes: true });
608
+ entries = await fs4.readdir(dir, { withFileTypes: true });
417
609
  } catch {
418
610
  return;
419
611
  }
420
612
  for (const e of entries) {
421
613
  if (e.isDirectory()) {
422
614
  if (skipDirs.has(e.name)) continue;
423
- await visit(path3.join(dir, e.name), depth + 1);
615
+ await visit(path4.join(dir, e.name), depth + 1);
424
616
  } else if (e.isFile() && match(e.name)) {
425
- found.push(path3.join(dir, e.name));
617
+ found.push(path4.join(dir, e.name));
426
618
  }
427
619
  }
428
620
  }
@@ -432,7 +624,7 @@ async function walk(root, match, maxDepth = 5) {
432
624
  async function detectHints(cwd) {
433
625
  const hints = [];
434
626
  for (const fname of ["app.json", "app.config.json"]) {
435
- const txt = await readIfExists(path3.join(cwd, fname));
627
+ const txt = await readIfExists(path4.join(cwd, fname));
436
628
  if (!txt) continue;
437
629
  try {
438
630
  const json = JSON.parse(txt);
@@ -463,7 +655,7 @@ async function detectHints(cwd) {
463
655
  if (m?.[1]) {
464
656
  const pkg = m[1];
465
657
  if (!hints.some((h) => h.packageName === pkg)) {
466
- hints.push({ packageName: pkg, source: [path3.relative(cwd, f)] });
658
+ hints.push({ packageName: pkg, source: [path4.relative(cwd, f)] });
467
659
  }
468
660
  }
469
661
  }
@@ -478,7 +670,7 @@ async function detectHints(cwd) {
478
670
  let bid = m[1];
479
671
  if (bid.includes("$(PRODUCT_BUNDLE_IDENTIFIER)")) continue;
480
672
  if (!hints.some((h) => h.bundleId === bid)) {
481
- hints.push({ bundleId: bid, source: [path3.relative(cwd, f)] });
673
+ hints.push({ bundleId: bid, source: [path4.relative(cwd, f)] });
482
674
  }
483
675
  }
484
676
  }
@@ -491,11 +683,11 @@ async function detectHints(cwd) {
491
683
  const bid = m[1].trim().replace(/^["']|["']$/g, "");
492
684
  if (!bid || bid.includes("$")) continue;
493
685
  if (!hints.some((h) => h.bundleId === bid)) {
494
- hints.push({ bundleId: bid, source: [path3.relative(cwd, f)] });
686
+ hints.push({ bundleId: bid, source: [path4.relative(cwd, f)] });
495
687
  }
496
688
  }
497
689
  }
498
- const pkgJson = await readIfExists(path3.join(cwd, "package.json"));
690
+ const pkgJson = await readIfExists(path4.join(cwd, "package.json"));
499
691
  if (pkgJson) {
500
692
  try {
501
693
  const json = JSON.parse(pkgJson);
@@ -531,7 +723,7 @@ async function detectHints(cwd) {
531
723
  return merged.filter((h) => h.packageName || h.bundleId);
532
724
  }
533
725
  async function hasAnyProjectSignal(cwd) {
534
- return await pathExists(path3.join(cwd, "package.json")) || await pathExists(path3.join(cwd, "app.json")) || await pathExists(path3.join(cwd, "android")) || await pathExists(path3.join(cwd, "ios"));
726
+ return await pathExists(path4.join(cwd, "package.json")) || await pathExists(path4.join(cwd, "app.json")) || await pathExists(path4.join(cwd, "android")) || await pathExists(path4.join(cwd, "ios"));
535
727
  }
536
728
 
537
729
  // src/deploy.ts
@@ -539,28 +731,28 @@ import kleur from "kleur";
539
731
  import * as readline from "readline";
540
732
 
541
733
  // src/config.ts
542
- import fs4 from "fs/promises";
543
- import path4 from "path";
544
- import os3 from "os";
545
- var CONFIG_DIR2 = path4.join(os3.homedir(), ".mimi-seed");
546
- var CONFIG_PATH = path4.join(CONFIG_DIR2, "config.json");
734
+ import fs5 from "fs/promises";
735
+ import path5 from "path";
736
+ import os4 from "os";
737
+ var CONFIG_DIR2 = path5.join(os4.homedir(), ".mimi-seed");
738
+ var CONFIG_PATH = path5.join(CONFIG_DIR2, "config.json");
547
739
  async function readConfig() {
548
740
  try {
549
- const txt = await fs4.readFile(CONFIG_PATH, "utf8");
741
+ const txt = await fs5.readFile(CONFIG_PATH, "utf8");
550
742
  return JSON.parse(txt);
551
743
  } catch {
552
744
  return null;
553
745
  }
554
746
  }
555
747
  async function writeConfig(cfg) {
556
- await fs4.mkdir(CONFIG_DIR2, { recursive: true });
557
- await fs4.writeFile(CONFIG_PATH, JSON.stringify(cfg, null, 2));
748
+ await fs5.mkdir(CONFIG_DIR2, { recursive: true });
749
+ await fs5.writeFile(CONFIG_PATH, JSON.stringify(cfg, null, 2));
558
750
  if (process.platform !== "win32") {
559
- await fs4.chmod(CONFIG_PATH, 384);
751
+ await fs5.chmod(CONFIG_PATH, 384);
560
752
  }
561
753
  }
562
754
  async function deleteConfig() {
563
- await fs4.rm(CONFIG_PATH, { force: true });
755
+ await fs5.rm(CONFIG_PATH, { force: true });
564
756
  }
565
757
  var CONFIG_LOCATION = CONFIG_PATH;
566
758
  async function getEffectiveConfig() {
@@ -579,13 +771,13 @@ async function getEffectiveConfig() {
579
771
  }
580
772
 
581
773
  // src/ci-providers.ts
582
- import fs5 from "fs";
583
- import path5 from "path";
584
- import os4 from "os";
585
- var CI_CONFIG_PATH = path5.join(os4.homedir(), ".mimi-seed", "ci.json");
774
+ import fs6 from "fs";
775
+ import path6 from "path";
776
+ import os5 from "os";
777
+ var CI_CONFIG_PATH = path6.join(os5.homedir(), ".mimi-seed", "ci.json");
586
778
  function loadCiProviderConfig() {
587
779
  try {
588
- const cfg = JSON.parse(fs5.readFileSync(CI_CONFIG_PATH, "utf-8"));
780
+ const cfg = JSON.parse(fs6.readFileSync(CI_CONFIG_PATH, "utf-8"));
589
781
  const host = normalizeHost(cfg.host);
590
782
  return host ? { ...cfg, host } : { ...cfg, host: void 0 };
591
783
  } catch {
@@ -599,15 +791,15 @@ function normalizeHost(host) {
599
791
  return withScheme.replace(/\/+$/, "");
600
792
  }
601
793
  function saveCiProviderConfig(cfg) {
602
- const dir = path5.dirname(CI_CONFIG_PATH);
603
- if (!fs5.existsSync(dir)) {
604
- fs5.mkdirSync(dir, { recursive: true, mode: 448 });
794
+ const dir = path6.dirname(CI_CONFIG_PATH);
795
+ if (!fs6.existsSync(dir)) {
796
+ fs6.mkdirSync(dir, { recursive: true, mode: 448 });
605
797
  }
606
798
  const normalized = { ...cfg, host: normalizeHost(cfg.host) };
607
799
  if (!normalized.host) delete normalized.host;
608
- fs5.writeFileSync(CI_CONFIG_PATH, JSON.stringify(normalized, null, 2));
800
+ fs6.writeFileSync(CI_CONFIG_PATH, JSON.stringify(normalized, null, 2));
609
801
  if (process.platform !== "win32") {
610
- fs5.chmodSync(CI_CONFIG_PATH, 384);
802
+ fs6.chmodSync(CI_CONFIG_PATH, 384);
611
803
  }
612
804
  }
613
805
  async function verifyCiToken(cfg) {
@@ -1125,6 +1317,210 @@ async function cmdDeploy(argv) {
1125
1317
  log(kleur.dim(" https://play.google.com/console/developers"));
1126
1318
  }
1127
1319
 
1320
+ // src/i18n.ts
1321
+ var ko = {
1322
+ common: {
1323
+ yes: "y",
1324
+ optional: "\uC120\uD0DD",
1325
+ required: "\uD544\uC218",
1326
+ skip: "\uAC74\uB108\uB700",
1327
+ cancelled: "\uCDE8\uC18C\uB428",
1328
+ unknownCommand: (cmd) => `\uC54C \uC218 \uC5C6\uB294 \uBA85\uB839: ${cmd}`,
1329
+ error: (msg) => `\uC624\uB958: ${msg}`,
1330
+ checkWith: "\uC810\uAC80: mimi-seed doctor"
1331
+ },
1332
+ lang: {
1333
+ ask: " \uC5B8\uC5B4\uB97C \uC120\uD0DD\uD574\uC918 [1] \uD55C\uAD6D\uC5B4 [2] English (\uC5D4\uD130=\uD55C\uAD6D\uC5B4): ",
1334
+ saved: (l) => ` \u2705 \uC5B8\uC5B4: ${l === "ko" ? "\uD55C\uAD6D\uC5B4" : "English"} (\uB098\uC911\uC5D0 \uBC14\uAFB8\uAE30: mimi-seed lang en)`,
1335
+ usage: `${"mimi-seed lang"} \u2014 CLI \uCD9C\uB825 \uC5B8\uC5B4
1336
+
1337
+ mimi-seed lang \uD604\uC7AC \uC5B8\uC5B4 \uD45C\uC2DC
1338
+ mimi-seed lang ko \uD55C\uAD6D\uC5B4
1339
+ mimi-seed lang en English
1340
+
1341
+ \uD658\uACBD\uBCC0\uC218 MIMI_SEED_LANG \uAC00 \uC788\uC73C\uBA74 \uADF8\uAC8C \uC6B0\uC120\uD569\uB2C8\uB2E4.`,
1342
+ current: (l) => `\uD604\uC7AC \uC5B8\uC5B4: ${l === "ko" ? "\uD55C\uAD6D\uC5B4 (ko)" : "English (en)"}`,
1343
+ invalid: (v) => `\uC54C \uC218 \uC5C6\uB294 \uC5B8\uC5B4: ${v} (ko \uB610\uB294 en)`
1344
+ },
1345
+ setup: {
1346
+ title: "mimi-seed setup",
1347
+ platformsDetected: (p) => ` \uAC10\uC9C0\uB41C \uD50C\uB7AB\uD3FC: ${p}`,
1348
+ statusTitle: "\uC5F0\uACB0 \uC0C1\uD0DC",
1349
+ statusDir: "(~/.mimi-seed)",
1350
+ groupCore: "\uD575\uC2EC",
1351
+ groupCi: "\uBE4C\uB4DC / CI",
1352
+ groupMarketing: "\uB9C8\uCF00\uD305 \xB7 AI",
1353
+ fallbackWorking: "\uD3F4\uBC31\uC73C\uB85C \uB3D9\uC791 \uC911",
1354
+ missingRequired: " \uD544\uC218 \uD56D\uBAA9 \uB204\uB77D:",
1355
+ cannotInteract: " \u2717 \uC774 \uC790\uACA9\uC99D\uBA85\uC740 \uB300\uD654\uD615 \uC785\uB825\uC774 \uD544\uC694\uD574\uC11C \uC5EC\uAE30\uC11C\uB294 \uC124\uC815\uD560 \uC218 \uC5C6\uC5B4:",
1356
+ cannotInteractHint: " \uD130\uBBF8\uB110\uC5D0\uC11C \uC2E4\uD589\uD574\uC918 (Git Bash \uB4F1 TTY \uBBF8\uAC10\uC9C0 \uD658\uACBD\uC774\uBA74 --interactive).",
1357
+ runInTerminal: " \uB300\uD654\uD615\uC73C\uB85C \uC5F0\uACB0\uD558\uB824\uBA74 \uD130\uBBF8\uB110\uC5D0\uC11C: mimi-seed setup",
1358
+ onlyAlreadyDone: " \u2705 \uC694\uCCAD\uD55C \uD56D\uBAA9\uC740 \uC774\uBBF8 \uC5F0\uACB0\uB3FC \uC788\uC5B4.",
1359
+ onlyReconnectHint: " \uB2E4\uC2DC \uC124\uC815\uD558\uB824\uBA74: mimi-seed setup --reconnect <id>",
1360
+ allDone: " \u2705 \uC5F0\uACB0\uD560 \uAC8C \uB354 \uC5C6\uC5B4. \uB2E4 \uB410\uB2E4.",
1361
+ planCount: (n) => ` ${n}\uAC1C \uD56D\uBAA9\uC744 \uC21C\uC11C\uB300\uB85C \uBB3C\uC5B4\uBCFC\uAC8C. \uC5B8\uC81C\uB4E0 s=\uAC74\uB108\uB6F0\uAE30, q=\uC885\uB8CC.`,
1362
+ prompt: " [c] \uC5F0\uACB0 [s] \uAC74\uB108\uB6F0\uAE30 [?] \uC774\uAC74 \uC5B4\uB5BB\uAC8C \uAD6C\uD558\uB098\uC694 [q] \uC885\uB8CC : ",
1363
+ promptInvalid: " c / s / ? / q \uC911\uC5D0\uC11C \uACE8\uB77C\uC918.",
1364
+ quit: " \uC911\uB2E8\uD588\uC5B4. \uC774\uC5B4\uC11C \uD558\uB824\uBA74 \uB2E4\uC2DC: mimi-seed setup",
1365
+ skipped: (fix) => ` \uAC74\uB108\uB700. \uB098\uC911\uC5D0: ${fix}`,
1366
+ obtainTitle: (label) => ` ${label} \u2014 \uBBF8\uB9AC \uC900\uBE44\uD560 \uAC83`,
1367
+ obtainMore: (anchor) => ` \uC790\uC138\uD788: docs/credentials.md#${anchor}`,
1368
+ neededFor: (platform) => `(${platform} \uBC30\uD3EC\uC5D0 \uD544\uC694)`,
1369
+ binFailed: (label, code, fix) => ` \u26A0 ${label} \uC124\uC815\uC774 \uC644\uB8CC\uB418\uC9C0 \uC54A\uC558\uC5B4 (exit ${code}). \uB098\uC911\uC5D0 \uB2E4\uC2DC: ${fix}`,
1370
+ verifying: " \u{1F50E} \uD1A0\uD070 \uAC80\uC99D \uC911...",
1371
+ verifyFailed: (reason) => ` \u274C \uD1A0\uD070 \uAC80\uC99D \uC2E4\uD328: ${reason}`,
1372
+ notSaved: (fix) => ` \uC800\uC7A5\uD558\uC9C0 \uC54A\uC558\uC5B4. \uB2E4\uC2DC: ${fix}`,
1373
+ ciSaved: (label, who) => ` \u2705 ${label} \uC5F0\uACB0\uB428${who} \u2192 ~/.mimi-seed/ci.json`,
1374
+ runSeparately: (cmd) => ` \uC774\uAC74 \uBCC4\uB3C4 \uBA85\uB839\uC73C\uB85C \uC2E4\uD589\uD574\uC918: ${cmd}`,
1375
+ envVar: " \uD658\uACBD\uBCC0\uC218\uB85C \uC124\uC815\uD558\uB294 \uD56D\uBAA9\uC774\uC57C:",
1376
+ pressEnter: " (\uC5D4\uD130\uB97C \uB204\uB974\uBA74 \uACC4\uC18D) ",
1377
+ stillMissing: " \uC544\uC9C1 \uD544\uC218 \uD56D\uBAA9\uC774 \uB0A8\uC544 \uC788\uC5B4:",
1378
+ requiredDone: " \u2705 \uD544\uC218 \uC5F0\uACB0 \uC644\uB8CC.",
1379
+ nextSteps: " \uC810\uAC80: mimi-seed doctor \xB7 \uBC30\uD3EC: mimi-seed deploy"
1380
+ },
1381
+ doctor: {
1382
+ title: "mimi-seed doctor",
1383
+ secAuth: "\uC778\uC99D",
1384
+ secCreds: "\uB85C\uCEEC \uC790\uACA9\uC99D\uBA85 (~/.mimi-seed)",
1385
+ secEnv: "\uB85C\uCEEC \uD658\uACBD",
1386
+ secApps: "\uC571 \uAC10\uC9C0",
1387
+ noToken: "Mimi Seed \uD1A0\uD070 \uC5C6\uC74C",
1388
+ noTokenFix: "`mimi-seed init` \uC2E4\uD589 \uD544\uC694",
1389
+ tokenSaved: "\uD1A0\uD070 \uC800\uC7A5\uB428",
1390
+ endpoint: "\uC5D4\uB4DC\uD3EC\uC778\uD2B8",
1391
+ ciMode: "CI \uBAA8\uB4DC",
1392
+ ciModeDetail: "MIMI_SEED_TOKEN \uD658\uACBD\uBCC0\uC218 \uC0AC\uC6A9 \uC911",
1393
+ tokenInvalid: "\uD1A0\uD070 \uAC80\uC99D \uC2E4\uD328",
1394
+ serverOk: "Mimi Seed \uC11C\uBC84 \uC5F0\uACB0\uB428",
1395
+ appCount: (n) => `\uC571 ${n}\uAC1C`,
1396
+ unknownService: (id) => `${id} (\uC54C \uC218 \uC5C6\uB294 \uC11C\uBE44\uC2A4)`,
1397
+ credsHint: " \uC804\uBD80 \uC5F0\uACB0\uD558\uAE30: mimi-seed setup \xB7 OAuth \uC2E0\uC120\uB3C4: mimi-seed auth status\n",
1398
+ nodeTooOld: (v) => `${v} \u2014 v20 \uC774\uC0C1 \uD544\uC694 (.nvmrc \uCC38\uACE0)`,
1399
+ gitRepo: "Git \uC800\uC7A5\uC18C",
1400
+ gitTag: (t2) => `\uCD5C\uC2E0 \uD0DC\uADF8: ${t2}`,
1401
+ gitCommits: (n) => `\uCEE4\uBC0B ${n}\uAC1C`,
1402
+ noGit: "Git \uC800\uC7A5\uC18C \uC5C6\uC74C",
1403
+ noGitDetail: "mimi-seed notes \uC0AC\uC6A9 \uBD88\uAC00",
1404
+ noApp: "\uC571 \uAC10\uC9C0 \uC5C6\uC74C",
1405
+ noAppDetail: "app.json / build.gradle / Info.plist \uC5C6\uC74C",
1406
+ unnamed: "(\uC774\uB984 \uBBF8\uC0C1)",
1407
+ requirements: (proj) => `${proj} \uC694\uAD6C\uC0AC\uD56D (.mimi-seed.json)`,
1408
+ thisProject: "\uC774 \uD504\uB85C\uC81D\uD2B8"
1409
+ },
1410
+ auth: {
1411
+ title: "mimi-seed auth \u2014 \uB85C\uCEEC \uC790\uACA9\uC99D\uBA85 \uC778\uC99D/\uAD00\uB9AC",
1412
+ statusTitle: "\uB85C\uCEEC \uC790\uACA9\uC99D\uBA85 \uC0C1\uD0DC",
1413
+ connectAll: "\n \uD55C \uBC88\uC5D0 \uC5F0\uACB0: mimi-seed setup",
1414
+ unknownSub: (sub) => `\uC54C \uC218 \uC5C6\uB294 auth \uC11C\uBE0C\uBA85\uB839: ${sub}`,
1415
+ npxFailed: (cmd, msg) => `
1416
+ \u274C ${cmd} \uC2E4\uD589 \uC2E4\uD328: ${msg}
1417
+ `
1418
+ }
1419
+ };
1420
+ var en = {
1421
+ common: {
1422
+ yes: "y",
1423
+ optional: "optional",
1424
+ required: "required",
1425
+ skip: "skipped",
1426
+ cancelled: "Cancelled",
1427
+ unknownCommand: (cmd) => `Unknown command: ${cmd}`,
1428
+ error: (msg) => `Error: ${msg}`,
1429
+ checkWith: "Check with: mimi-seed doctor"
1430
+ },
1431
+ lang: {
1432
+ ask: " Choose a language [1] \uD55C\uAD6D\uC5B4 [2] English (Enter = \uD55C\uAD6D\uC5B4): ",
1433
+ saved: (l) => ` \u2705 Language: ${l === "ko" ? "\uD55C\uAD6D\uC5B4" : "English"} (change later: mimi-seed lang ko)`,
1434
+ usage: `mimi-seed lang \u2014 CLI output language
1435
+
1436
+ mimi-seed lang show current language
1437
+ mimi-seed lang ko \uD55C\uAD6D\uC5B4
1438
+ mimi-seed lang en English
1439
+
1440
+ MIMI_SEED_LANG takes precedence when set.`,
1441
+ current: (l) => `Current language: ${l === "ko" ? "\uD55C\uAD6D\uC5B4 (ko)" : "English (en)"}`,
1442
+ invalid: (v) => `Unknown language: ${v} (use ko or en)`
1443
+ },
1444
+ setup: {
1445
+ title: "mimi-seed setup",
1446
+ platformsDetected: (p) => ` Detected platforms: ${p}`,
1447
+ statusTitle: "Connection status",
1448
+ statusDir: "(~/.mimi-seed)",
1449
+ groupCore: "Core",
1450
+ groupCi: "Build / CI",
1451
+ groupMarketing: "Marketing \xB7 AI",
1452
+ fallbackWorking: "working via fallback",
1453
+ missingRequired: " Missing required:",
1454
+ cannotInteract: " \u2717 These need interactive input and cannot be set up here:",
1455
+ cannotInteractHint: " Run it in a terminal (add --interactive if your shell hides the TTY, e.g. Git Bash).",
1456
+ runInTerminal: " To connect interactively, run: mimi-seed setup",
1457
+ onlyAlreadyDone: " \u2705 What you asked for is already connected.",
1458
+ onlyReconnectHint: " To redo it: mimi-seed setup --reconnect <id>",
1459
+ allDone: " \u2705 Nothing left to connect. You're set.",
1460
+ planCount: (n) => ` I'll walk you through ${n} item(s). s = skip, q = quit, anytime.`,
1461
+ prompt: " [c] connect [s] skip [?] how do I get this [q] quit : ",
1462
+ promptInvalid: " Please choose c / s / ? / q.",
1463
+ quit: " Stopped. To pick up where you left off: mimi-seed setup",
1464
+ skipped: (fix) => ` Skipped. Later: ${fix}`,
1465
+ obtainTitle: (label) => ` ${label} \u2014 what to get first`,
1466
+ obtainMore: (anchor) => ` Details: docs/credentials.md#${anchor}`,
1467
+ neededFor: (platform) => `(needed to ship to ${platform})`,
1468
+ binFailed: (label, code, fix) => ` \u26A0 ${label} was not completed (exit ${code}). Try again later: ${fix}`,
1469
+ verifying: " \u{1F50E} Verifying token...",
1470
+ verifyFailed: (reason) => ` \u274C Token verification failed: ${reason}`,
1471
+ notSaved: (fix) => ` Nothing was saved. Retry: ${fix}`,
1472
+ ciSaved: (label, who) => ` \u2705 ${label} connected${who} \u2192 ~/.mimi-seed/ci.json`,
1473
+ runSeparately: (cmd) => ` Run this one separately: ${cmd}`,
1474
+ envVar: " This one is set through an environment variable:",
1475
+ pressEnter: " (press Enter to continue) ",
1476
+ stillMissing: " Still missing, and required:",
1477
+ requiredDone: " \u2705 All required credentials connected.",
1478
+ nextSteps: " Check: mimi-seed doctor \xB7 Ship: mimi-seed deploy"
1479
+ },
1480
+ doctor: {
1481
+ title: "mimi-seed doctor",
1482
+ secAuth: "Account",
1483
+ secCreds: "Local credentials (~/.mimi-seed)",
1484
+ secEnv: "Environment",
1485
+ secApps: "App detection",
1486
+ noToken: "No Mimi Seed token",
1487
+ noTokenFix: "run `mimi-seed init`",
1488
+ tokenSaved: "Token stored",
1489
+ endpoint: "Endpoint",
1490
+ ciMode: "CI mode",
1491
+ ciModeDetail: "using MIMI_SEED_TOKEN",
1492
+ tokenInvalid: "Token rejected",
1493
+ serverOk: "Connected to Mimi Seed",
1494
+ appCount: (n) => `${n} app(s)`,
1495
+ unknownService: (id) => `${id} (unknown service)`,
1496
+ credsHint: " Connect everything: mimi-seed setup \xB7 OAuth freshness: mimi-seed auth status\n",
1497
+ nodeTooOld: (v) => `${v} \u2014 v20+ required (see .nvmrc)`,
1498
+ gitRepo: "Git repository",
1499
+ gitTag: (t2) => `latest tag: ${t2}`,
1500
+ gitCommits: (n) => `${n} commit(s)`,
1501
+ noGit: "Not a git repository",
1502
+ noGitDetail: "mimi-seed notes is unavailable",
1503
+ noApp: "No app detected",
1504
+ noAppDetail: "no app.json / build.gradle / Info.plist",
1505
+ unnamed: "(unnamed)",
1506
+ requirements: (proj) => `${proj} requirements (.mimi-seed.json)`,
1507
+ thisProject: "This project"
1508
+ },
1509
+ auth: {
1510
+ title: "mimi-seed auth \u2014 local credential setup",
1511
+ statusTitle: "Local credential status",
1512
+ connectAll: "\n Connect everything at once: mimi-seed setup",
1513
+ unknownSub: (sub) => `Unknown auth subcommand: ${sub}`,
1514
+ npxFailed: (cmd, msg) => `
1515
+ \u274C Failed to run ${cmd}: ${msg}
1516
+ `
1517
+ }
1518
+ };
1519
+ var CATALOGS = { ko, en };
1520
+ function t() {
1521
+ return CATALOGS[resolveLang()];
1522
+ }
1523
+
1128
1524
  // src/setup.ts
1129
1525
  function log2(msg = "") {
1130
1526
  process.stdout.write(msg + "\n");
@@ -1169,15 +1565,16 @@ function statusLine(spec, detected, platforms) {
1169
1565
  else if (isSatisfied(spec, detected)) mark = kleur2.yellow("~");
1170
1566
  else if (spec.requirement === "optional" || !relevant) mark = kleur2.dim("\xB7");
1171
1567
  else mark = kleur2.red("\u2717");
1172
- const tail = d.present ? kleur2.dim(d.detail ?? "") : isSatisfied(spec, detected) ? kleur2.dim(`${spec.note ?? "\uD3F4\uBC31\uC73C\uB85C \uB3D9\uC791 \uC911"}`) : kleur2.dim(`\u2192 ${spec.fix}`);
1173
- return ` ${mark} ${spec.label.padEnd(24)} ${tail}`;
1568
+ const tail = d.present ? kleur2.dim(d.detail ?? "") : isSatisfied(spec, detected) ? kleur2.dim(`${credNote(spec) ?? t().setup.fallbackWorking}`) : kleur2.dim(`\u2192 ${spec.fix}`);
1569
+ return ` ${mark} ${credLabel(spec).padEnd(24)} ${tail}`;
1174
1570
  }
1175
1571
  function printStatus(detected, platforms) {
1176
- log2(kleur2.bold("\uC5F0\uACB0 \uC0C1\uD0DC ") + kleur2.dim("(~/.mimi-seed)"));
1572
+ const m = t().setup;
1573
+ log2(kleur2.bold(m.statusTitle + " ") + kleur2.dim(m.statusDir));
1177
1574
  log2();
1178
1575
  for (const group of ["core", "ci", "marketing"]) {
1179
1576
  const specs = CREDENTIALS.filter((c) => c.group === group);
1180
- const title = group === "core" ? "\uD575\uC2EC" : group === "ci" ? "\uBE4C\uB4DC / CI" : "\uB9C8\uCF00\uD305 \xB7 AI";
1577
+ const title = group === "core" ? m.groupCore : group === "ci" ? m.groupCi : m.groupMarketing;
1181
1578
  log2(kleur2.dim(` \u2500\u2500 ${title} \u2500\u2500`));
1182
1579
  for (const spec of specs) log2(statusLine(spec, detected, platforms));
1183
1580
  log2();
@@ -1186,70 +1583,86 @@ function printStatus(detected, platforms) {
1186
1583
  function ask(q) {
1187
1584
  const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
1188
1585
  return new Promise((resolve) => {
1189
- rl.question(q, (a) => {
1586
+ let done = false;
1587
+ const finish = (value) => {
1588
+ if (done) return;
1589
+ done = true;
1190
1590
  rl.close();
1191
- resolve(a.trim());
1192
- });
1193
- rl.on("close", () => resolve(""));
1591
+ resolve(value);
1592
+ };
1593
+ rl.question(q, (a) => finish(a.trim()));
1594
+ rl.on("close", () => finish(""));
1194
1595
  });
1195
1596
  }
1196
1597
  function printObtain(spec) {
1197
1598
  log2();
1198
- log2(kleur2.bold(` ${spec.label} \u2014 \uBBF8\uB9AC \uC900\uBE44\uD560 \uAC83`));
1199
- for (const line of spec.obtain) log2(kleur2.dim(` ${line}`));
1599
+ log2(kleur2.bold(t().setup.obtainTitle(credLabel(spec))));
1600
+ for (const line of credObtain(spec)) log2(kleur2.dim(` ${line}`));
1200
1601
  if (spec.docsAnchor) {
1201
- log2(kleur2.dim(` \uC790\uC138\uD788: docs/credentials.md#${spec.docsAnchor}`));
1602
+ log2(kleur2.dim(t().setup.obtainMore(spec.docsAnchor)));
1202
1603
  }
1203
1604
  log2();
1204
1605
  }
1606
+ async function ensureLangChosen() {
1607
+ if (!isLangUnset()) return;
1608
+ const answer = await ask(t().lang.ask);
1609
+ const lang = answer.trim() === "2" || answer.trim().toLowerCase() === "en" ? "en" : "ko";
1610
+ writeSettings({ lang });
1611
+ process.env.MIMI_SEED_LANG = lang;
1612
+ log2(kleur2.green(t().lang.saved(lang)));
1613
+ log2();
1614
+ }
1205
1615
  async function connectOne(spec) {
1206
1616
  switch (spec.setup.kind) {
1207
1617
  case "mcp-bin": {
1208
1618
  const code = await runMcpBin(spec.setup.bin, spec.setup.args ?? []);
1209
1619
  if (code !== 0) {
1210
- log2(kleur2.yellow(` \u26A0 ${spec.label} \uC124\uC815\uC774 \uC644\uB8CC\uB418\uC9C0 \uC54A\uC558\uC5B4 (exit ${code}). \uB098\uC911\uC5D0 \uB2E4\uC2DC: ${spec.fix}`));
1620
+ log2(kleur2.yellow(t().setup.binFailed(credLabel(spec), code, spec.fix)));
1211
1621
  }
1212
1622
  return true;
1213
1623
  }
1214
1624
  case "cli": {
1215
1625
  const provider = spec.setup.handler;
1216
1626
  const cfg = await promptGitProviderSetup(provider);
1217
- log2(kleur2.dim(" \u{1F50E} \uD1A0\uD070 \uAC80\uC99D \uC911..."));
1627
+ log2(kleur2.dim(t().setup.verifying));
1218
1628
  const check = await verifyCiToken(cfg);
1219
1629
  if (!check.ok) {
1220
- log2(kleur2.red(` \u274C \uD1A0\uD070 \uAC80\uC99D \uC2E4\uD328: ${check.reason}`));
1221
- log2(kleur2.dim(` \uC800\uC7A5\uD558\uC9C0 \uC54A\uC558\uC5B4. \uB2E4\uC2DC: ${spec.fix}`));
1630
+ log2(kleur2.red(t().setup.verifyFailed(check.reason ?? "")));
1631
+ log2(kleur2.dim(t().setup.notSaved(spec.fix)));
1222
1632
  return true;
1223
1633
  }
1224
1634
  saveCiProviderConfig(cfg);
1225
- log2(kleur2.green(` \u2705 ${spec.label} \uC5F0\uACB0\uB428${check.login ? ` (${check.login})` : ""} \u2192 ~/.mimi-seed/ci.json`));
1635
+ log2(kleur2.green(t().setup.ciSaved(credLabel(spec), check.login ? ` (${check.login})` : "")));
1226
1636
  return true;
1227
1637
  }
1228
1638
  case "command": {
1229
- log2(kleur2.yellow(` \uC774\uAC74 \uBCC4\uB3C4 \uBA85\uB839\uC73C\uB85C \uC2E4\uD589\uD574\uC918: ${kleur2.cyan(spec.setup.run)}`));
1230
- await ask(kleur2.dim(" (\uC5D4\uD130\uB97C \uB204\uB974\uBA74 \uACC4\uC18D) "));
1639
+ log2(kleur2.yellow(t().setup.runSeparately(kleur2.cyan(spec.setup.run))));
1640
+ await ask(kleur2.dim(t().setup.pressEnter));
1231
1641
  return true;
1232
1642
  }
1233
1643
  case "env": {
1234
- log2(kleur2.yellow(` \uD658\uACBD\uBCC0\uC218\uB85C \uC124\uC815\uD558\uB294 \uD56D\uBAA9\uC774\uC57C:`));
1644
+ log2(kleur2.yellow(t().setup.envVar));
1235
1645
  log2(kleur2.cyan(` export ${spec.setup.envVar}=...`));
1236
1646
  log2(kleur2.dim(` (Windows PowerShell: $env:${spec.setup.envVar} = "..." )`));
1237
- await ask(kleur2.dim(" (\uC5D4\uD130\uB97C \uB204\uB974\uBA74 \uACC4\uC18D) "));
1647
+ await ask(kleur2.dim(t().setup.pressEnter));
1238
1648
  return true;
1239
1649
  }
1240
1650
  }
1241
1651
  }
1242
1652
  async function cmdSetup(argv) {
1243
1653
  const opts = parseSetupArgs(argv);
1244
- const home = os5.homedir();
1654
+ const home = os6.homedir();
1245
1655
  migrateLegacyJenkins(home);
1656
+ if (resolveMode(opts, process.env, process.stdin.isTTY) === "interactive") {
1657
+ await ensureLangChosen();
1658
+ }
1246
1659
  const hints = await detectHints(process.cwd());
1247
1660
  const platforms = opts.platforms ?? [
1248
1661
  hints.some((h) => h.packageName) ? "android" : null,
1249
1662
  hints.some((h) => h.bundleId) ? "ios" : null
1250
1663
  ].filter(Boolean);
1251
- log2(kleur2.bold("mimi-seed setup"));
1252
- if (platforms.length > 0) log2(kleur2.dim(` \uAC10\uC9C0\uB41C \uD50C\uB7AB\uD3FC: ${platforms.join(", ")}`));
1664
+ log2(kleur2.bold(t().setup.title));
1665
+ if (platforms.length > 0) log2(kleur2.dim(t().setup.platformsDetected(platforms.join(", "))));
1253
1666
  log2();
1254
1667
  let detected = detectAll(home);
1255
1668
  printStatus(detected, platforms);
@@ -1262,53 +1675,54 @@ async function cmdSetup(argv) {
1262
1675
  if (mode === "report-only") {
1263
1676
  const missing = missingRequired(detected, platforms);
1264
1677
  if (missing.length > 0) {
1265
- log2(kleur2.yellow(" \uD544\uC218 \uD56D\uBAA9 \uB204\uB77D:"));
1266
- for (const spec of missing) log2(kleur2.yellow(` \u2022 ${spec.label} \u2192 ${spec.fix}`));
1678
+ log2(kleur2.yellow(t().setup.missingRequired));
1679
+ for (const spec of missing) log2(kleur2.yellow(` \u2022 ${credLabel(spec)} \u2192 ${spec.fix}`));
1267
1680
  log2();
1268
1681
  }
1269
1682
  if (opts.only && plan.length > 0) {
1270
- log2(kleur2.red(" \u2717 \uC774 \uC790\uACA9\uC99D\uBA85\uC740 \uB300\uD654\uD615 \uC785\uB825\uC774 \uD544\uC694\uD574\uC11C \uC5EC\uAE30\uC11C\uB294 \uC124\uC815\uD560 \uC218 \uC5C6\uC5B4:"));
1271
- for (const spec of plan) log2(kleur2.red(` \u2022 ${spec.label}`));
1272
- log2(kleur2.dim(" \uD130\uBBF8\uB110\uC5D0\uC11C \uC2E4\uD589\uD574\uC918 (Git Bash \uB4F1 TTY \uBBF8\uAC10\uC9C0 \uD658\uACBD\uC774\uBA74 --interactive)."));
1683
+ log2(kleur2.red(t().setup.cannotInteract));
1684
+ for (const spec of plan) log2(kleur2.red(` \u2022 ${credLabel(spec)}`));
1685
+ log2(kleur2.dim(t().setup.cannotInteractHint));
1273
1686
  process.exit(1);
1274
1687
  }
1275
- log2(kleur2.dim(" \uB300\uD654\uD615\uC73C\uB85C \uC5F0\uACB0\uD558\uB824\uBA74 \uD130\uBBF8\uB110\uC5D0\uC11C: mimi-seed setup"));
1688
+ log2(kleur2.dim(t().setup.runInTerminal));
1276
1689
  if (opts.failOnMissing && missing.length > 0) process.exit(1);
1277
1690
  return;
1278
1691
  }
1279
1692
  if (plan.length === 0) {
1280
1693
  if (opts.only) {
1281
- log2(kleur2.green(" \u2705 \uC694\uCCAD\uD55C \uD56D\uBAA9\uC740 \uC774\uBBF8 \uC5F0\uACB0\uB3FC \uC788\uC5B4."));
1282
- log2(kleur2.dim(" \uB2E4\uC2DC \uC124\uC815\uD558\uB824\uBA74: mimi-seed setup --reconnect <id>"));
1694
+ log2(kleur2.green(t().setup.onlyAlreadyDone));
1695
+ log2(kleur2.dim(t().setup.onlyReconnectHint));
1283
1696
  } else {
1284
- log2(kleur2.green(" \u2705 \uC5F0\uACB0\uD560 \uAC8C \uB354 \uC5C6\uC5B4. \uB2E4 \uB410\uB2E4."));
1697
+ log2(kleur2.green(t().setup.allDone));
1285
1698
  }
1286
- log2(kleur2.dim(" \uC810\uAC80: mimi-seed doctor"));
1699
+ log2(kleur2.dim(" " + t().common.checkWith));
1287
1700
  if (opts.failOnMissing && missingRequired(detected, platforms).length > 0) process.exit(1);
1288
1701
  return;
1289
1702
  }
1290
- log2(kleur2.dim(` ${plan.length}\uAC1C \uD56D\uBAA9\uC744 \uC21C\uC11C\uB300\uB85C \uBB3C\uC5B4\uBCFC\uAC8C. \uC5B8\uC81C\uB4E0 s=\uAC74\uB108\uB6F0\uAE30, q=\uC885\uB8CC.`));
1703
+ log2(kleur2.dim(t().setup.planCount(plan.length)));
1291
1704
  log2();
1292
1705
  let aborted = false;
1293
1706
  for (const spec of plan) {
1294
1707
  if (aborted) break;
1295
- const req = spec.requirement === "optional" ? kleur2.dim("(\uC120\uD0DD)") : spec.requirement === "platform" ? kleur2.dim(`(${spec.platform} \uBC30\uD3EC\uC5D0 \uD544\uC694)`) : kleur2.red("(\uD544\uC218)");
1708
+ const req = spec.requirement === "optional" ? kleur2.dim(`(${t().common.optional})`) : spec.requirement === "platform" ? kleur2.dim(t().setup.neededFor(spec.platform)) : kleur2.red(`(${t().common.required})`);
1296
1709
  for (; ; ) {
1297
- log2(kleur2.bold(`\u25B8 ${spec.label} ${req}`));
1298
- if (spec.note) log2(kleur2.dim(` ${spec.note}`));
1299
- const answer = (await ask(" [c] \uC5F0\uACB0 [s] \uAC74\uB108\uB6F0\uAE30 [?] \uC774\uAC74 \uC5B4\uB5BB\uAC8C \uAD6C\uD558\uB098\uC694 [q] \uC885\uB8CC : ")).toLowerCase();
1710
+ log2(kleur2.bold(`\u25B8 ${credLabel(spec)} ${req}`));
1711
+ const note = credNote(spec);
1712
+ if (note) log2(kleur2.dim(` ${note}`));
1713
+ const answer = (await ask(t().setup.prompt)).toLowerCase();
1300
1714
  if (answer === "?" || answer === "h") {
1301
1715
  printObtain(spec);
1302
1716
  continue;
1303
1717
  }
1304
1718
  if (answer === "q") {
1305
1719
  log2();
1306
- log2(kleur2.dim(" \uC911\uB2E8\uD588\uC5B4. \uC774\uC5B4\uC11C \uD558\uB824\uBA74 \uB2E4\uC2DC: mimi-seed setup"));
1720
+ log2(kleur2.dim(t().setup.quit));
1307
1721
  aborted = true;
1308
1722
  break;
1309
1723
  }
1310
1724
  if (answer === "s" || answer === "") {
1311
- log2(kleur2.dim(` \uAC74\uB108\uB700. \uB098\uC911\uC5D0: ${spec.fix}`));
1725
+ log2(kleur2.dim(t().setup.skipped(spec.fix)));
1312
1726
  log2();
1313
1727
  break;
1314
1728
  }
@@ -1317,7 +1731,7 @@ async function cmdSetup(argv) {
1317
1731
  log2();
1318
1732
  break;
1319
1733
  }
1320
- log2(kleur2.dim(" c / s / ? / q \uC911\uC5D0\uC11C \uACE8\uB77C\uC918."));
1734
+ log2(kleur2.dim(t().setup.promptInvalid));
1321
1735
  }
1322
1736
  }
1323
1737
  detected = detectAll(home);
@@ -1325,12 +1739,12 @@ async function cmdSetup(argv) {
1325
1739
  printStatus(detected, platforms);
1326
1740
  const stillMissing = missingRequired(detected, platforms);
1327
1741
  if (stillMissing.length === 0) {
1328
- log2(kleur2.green(" \u2705 \uD544\uC218 \uC5F0\uACB0 \uC644\uB8CC."));
1742
+ log2(kleur2.green(t().setup.requiredDone));
1329
1743
  } else {
1330
- log2(kleur2.yellow(" \uC544\uC9C1 \uD544\uC218 \uD56D\uBAA9\uC774 \uB0A8\uC544 \uC788\uC5B4:"));
1331
- for (const spec of stillMissing) log2(kleur2.yellow(` \u2022 ${spec.label} \u2192 ${spec.fix}`));
1744
+ log2(kleur2.yellow(t().setup.stillMissing));
1745
+ for (const spec of stillMissing) log2(kleur2.yellow(` \u2022 ${credLabel(spec)} \u2192 ${spec.fix}`));
1332
1746
  }
1333
- log2(kleur2.dim(" \uC810\uAC80: mimi-seed doctor \xB7 \uBC30\uD3EC: mimi-seed deploy"));
1747
+ log2(kleur2.dim(t().setup.nextSteps));
1334
1748
  log2();
1335
1749
  if (opts.failOnMissing && stillMissing.length > 0) process.exit(1);
1336
1750
  }
@@ -1342,11 +1756,17 @@ export {
1342
1756
  deleteConfig,
1343
1757
  CONFIG_LOCATION,
1344
1758
  getEffectiveConfig,
1759
+ writeSettings,
1760
+ isLang,
1761
+ resolveLang,
1762
+ credLabel,
1763
+ credNote,
1345
1764
  CREDENTIALS,
1346
1765
  tryCredById,
1347
1766
  detectAll,
1348
1767
  isSatisfied,
1349
1768
  migrateLegacyJenkins,
1769
+ t,
1350
1770
  MCP_PKG,
1351
1771
  runMcpBin,
1352
1772
  cmdDeploy,