mimi-seed 0.4.3 → 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.
- package/README.md +33 -4
- package/dist/chunk-R3LS4FY2.js +1776 -0
- package/dist/index.js +200 -880
- package/dist/setup-G2JY35UW.js +11 -0
- package/package.json +2 -2
|
@@ -0,0 +1,1776 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/setup.ts
|
|
4
|
+
import kleur2 from "kleur";
|
|
5
|
+
import * as readline2 from "readline";
|
|
6
|
+
import os6 from "os";
|
|
7
|
+
|
|
8
|
+
// src/credentials.ts
|
|
9
|
+
import fs2 from "fs";
|
|
10
|
+
import os2 from "os";
|
|
11
|
+
import path2 from "path";
|
|
12
|
+
|
|
13
|
+
// src/settings.ts
|
|
14
|
+
import fs from "fs";
|
|
15
|
+
import os from "os";
|
|
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
|
+
}
|
|
58
|
+
function credDir(home) {
|
|
59
|
+
return path2.join(home, ".mimi-seed");
|
|
60
|
+
}
|
|
61
|
+
function hasFile(home, name) {
|
|
62
|
+
return fs2.existsSync(path2.join(credDir(home), name));
|
|
63
|
+
}
|
|
64
|
+
function anyFileStarting(home, prefix) {
|
|
65
|
+
try {
|
|
66
|
+
return fs2.readdirSync(credDir(home)).some((f) => f.startsWith(prefix));
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function readJson(home, name) {
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(fs2.readFileSync(path2.join(credDir(home), name), "utf-8"));
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function hasPlaySa(home) {
|
|
79
|
+
if (hasFile(home, "play-service-account.json")) return true;
|
|
80
|
+
try {
|
|
81
|
+
return fs2.readdirSync(path2.join(credDir(home), "play-service-accounts")).some((f) => f.endsWith(".json"));
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function ciDetect(home, want) {
|
|
87
|
+
const ci = readJson(home, "ci.json");
|
|
88
|
+
if (!ci || ci.provider !== want) return { present: false };
|
|
89
|
+
return { present: true, detail: ci.owner && ci.repo ? `${ci.owner}/${ci.repo}` : void 0 };
|
|
90
|
+
}
|
|
91
|
+
var CREDENTIALS = [
|
|
92
|
+
{
|
|
93
|
+
id: "mimiseed",
|
|
94
|
+
label: {
|
|
95
|
+
ko: "Mimi Seed \uACC4\uC815 (\uD074\uB77C\uC6B0\uB4DC)",
|
|
96
|
+
en: "Mimi Seed account (cloud)"
|
|
97
|
+
},
|
|
98
|
+
requirement: "required",
|
|
99
|
+
group: "core",
|
|
100
|
+
file: "~/.mimi-seed/config.json",
|
|
101
|
+
fix: "mimi-seed init",
|
|
102
|
+
setup: { kind: "command", run: "mimi-seed init" },
|
|
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
|
+
},
|
|
113
|
+
docsAnchor: "cloud-pat",
|
|
114
|
+
detect: (home) => {
|
|
115
|
+
if (process.env.MIMI_SEED_TOKEN) return { present: true, detail: "MIMI_SEED_TOKEN (env)" };
|
|
116
|
+
const cfg = readJson(home, "config.json");
|
|
117
|
+
return cfg?.prefix ? { present: true, detail: `${cfg.prefix}\u2026` } : { present: false };
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: "oauth",
|
|
122
|
+
label: {
|
|
123
|
+
ko: "Google OAuth",
|
|
124
|
+
en: "Google OAuth"
|
|
125
|
+
},
|
|
126
|
+
requirement: "required",
|
|
127
|
+
group: "core",
|
|
128
|
+
file: "~/.mimi-seed/tokens.json",
|
|
129
|
+
fix: "mimi-seed auth login",
|
|
130
|
+
setup: { kind: "mcp-bin", bin: "mimi-seed-auth" },
|
|
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
|
+
},
|
|
145
|
+
docsAnchor: "google-oauth",
|
|
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
|
+
},
|
|
150
|
+
detect: (home) => ({ present: hasFile(home, "tokens.json") })
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: "appstore",
|
|
154
|
+
label: {
|
|
155
|
+
ko: "App Store Connect",
|
|
156
|
+
en: "App Store Connect"
|
|
157
|
+
},
|
|
158
|
+
requirement: "platform",
|
|
159
|
+
platform: "ios",
|
|
160
|
+
group: "core",
|
|
161
|
+
file: "~/.mimi-seed/appstore.json",
|
|
162
|
+
fix: "mimi-seed auth appstore",
|
|
163
|
+
setup: { kind: "mcp-bin", bin: "mimi-seed-appstore-auth" },
|
|
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
|
+
},
|
|
178
|
+
docsAnchor: "app-store-connect",
|
|
179
|
+
detect: (home) => {
|
|
180
|
+
const cfg = readJson(home, "appstore.json");
|
|
181
|
+
return cfg ? { present: true, detail: cfg.keyId ? `keyId ${cfg.keyId}` : void 0 } : { present: false };
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: "playstore",
|
|
186
|
+
label: {
|
|
187
|
+
ko: "Play \uC11C\uBE44\uC2A4 \uACC4\uC815",
|
|
188
|
+
en: "Play service account"
|
|
189
|
+
},
|
|
190
|
+
// 선택이다 — helpers.ts 의 requirePlayStoreAuth 가 SA → OAuth 로 폴백하고,
|
|
191
|
+
// 로그인 시 androidpublisher 스코프를 이미 받으므로 로컬 작업은 OAuth 로 된다.
|
|
192
|
+
requirement: "optional",
|
|
193
|
+
group: "core",
|
|
194
|
+
file: "~/.mimi-seed/play-service-account.json",
|
|
195
|
+
fallbackOn: ["oauth"],
|
|
196
|
+
fix: "mimi-seed auth playstore",
|
|
197
|
+
setup: { kind: "mcp-bin", bin: "mimi-seed-playstore-auth" },
|
|
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
|
+
},
|
|
216
|
+
docsAnchor: "play-service-account",
|
|
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
|
+
},
|
|
221
|
+
detect: (home) => ({ present: hasPlaySa(home) })
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
id: "bigquery",
|
|
225
|
+
label: {
|
|
226
|
+
ko: "BigQuery \uC11C\uBE44\uC2A4 \uACC4\uC815",
|
|
227
|
+
en: "BigQuery service account"
|
|
228
|
+
},
|
|
229
|
+
requirement: "optional",
|
|
230
|
+
group: "core",
|
|
231
|
+
file: "~/.mimi-seed/bigquery-service-account.json",
|
|
232
|
+
fallbackOn: ["oauth"],
|
|
233
|
+
fix: "mimi-seed auth bigquery",
|
|
234
|
+
setup: { kind: "mcp-bin", bin: "mimi-seed-bigquery-auth" },
|
|
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
|
+
},
|
|
251
|
+
docsAnchor: "bigquery",
|
|
252
|
+
note: {
|
|
253
|
+
ko: "\uC120\uD0DD \u2014 OAuth \uD3F4\uBC31 \uAC00\uB2A5",
|
|
254
|
+
en: "optional \u2014 falls back to OAuth"
|
|
255
|
+
},
|
|
256
|
+
detect: (home) => ({ present: anyFileStarting(home, "bigquery") })
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
id: "github",
|
|
260
|
+
label: {
|
|
261
|
+
ko: "GitHub Actions",
|
|
262
|
+
en: "GitHub Actions"
|
|
263
|
+
},
|
|
264
|
+
requirement: "optional",
|
|
265
|
+
group: "ci",
|
|
266
|
+
file: "~/.mimi-seed/ci.json",
|
|
267
|
+
fix: "mimi-seed auth ci",
|
|
268
|
+
setup: { kind: "cli", handler: "github" },
|
|
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
|
+
},
|
|
283
|
+
docsAnchor: "ci-github-gitlab",
|
|
284
|
+
detect: (home) => ciDetect(home, "github")
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
id: "gitlab",
|
|
288
|
+
label: {
|
|
289
|
+
ko: "GitLab CI",
|
|
290
|
+
en: "GitLab CI"
|
|
291
|
+
},
|
|
292
|
+
requirement: "optional",
|
|
293
|
+
group: "ci",
|
|
294
|
+
file: "~/.mimi-seed/ci.json",
|
|
295
|
+
fix: "mimi-seed auth ci",
|
|
296
|
+
setup: { kind: "cli", handler: "gitlab" },
|
|
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
|
+
},
|
|
307
|
+
docsAnchor: "ci-github-gitlab",
|
|
308
|
+
detect: (home) => ciDetect(home, "gitlab")
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
id: "jenkins",
|
|
312
|
+
label: {
|
|
313
|
+
ko: "Jenkins",
|
|
314
|
+
en: "Jenkins"
|
|
315
|
+
},
|
|
316
|
+
requirement: "optional",
|
|
317
|
+
group: "ci",
|
|
318
|
+
file: "~/.mimi-seed/jenkins.json",
|
|
319
|
+
fix: "mimi-seed auth jenkins",
|
|
320
|
+
setup: { kind: "mcp-bin", bin: "mimi-seed-jenkins-auth" },
|
|
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
|
+
},
|
|
333
|
+
docsAnchor: "jenkins",
|
|
334
|
+
detect: (home) => {
|
|
335
|
+
const cfg = readJson(home, "jenkins.json");
|
|
336
|
+
return cfg?.url ? { present: true, detail: cfg.url } : { present: false };
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
id: "googleads",
|
|
341
|
+
label: {
|
|
342
|
+
ko: "Google Ads",
|
|
343
|
+
en: "Google Ads"
|
|
344
|
+
},
|
|
345
|
+
requirement: "optional",
|
|
346
|
+
group: "marketing",
|
|
347
|
+
file: "~/.mimi-seed/google-ads.json",
|
|
348
|
+
fix: "mimi-seed auth googleads",
|
|
349
|
+
setup: { kind: "mcp-bin", bin: "mimi-seed-googleads-auth" },
|
|
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
|
+
},
|
|
366
|
+
docsAnchor: "google-ads",
|
|
367
|
+
detect: (home) => {
|
|
368
|
+
const cfg = readJson(home, "google-ads.json");
|
|
369
|
+
return cfg?.customerId ? { present: true, detail: cfg.customerId } : { present: false };
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
id: "facebook",
|
|
374
|
+
label: {
|
|
375
|
+
ko: "Facebook \uD398\uC774\uC9C0",
|
|
376
|
+
en: "Facebook Page"
|
|
377
|
+
},
|
|
378
|
+
requirement: "optional",
|
|
379
|
+
group: "marketing",
|
|
380
|
+
file: "~/.mimi-seed/facebook.json",
|
|
381
|
+
fix: "mimi-seed auth facebook",
|
|
382
|
+
setup: { kind: "mcp-bin", bin: "mimi-seed-social-auth", args: ["facebook"] },
|
|
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
|
+
},
|
|
397
|
+
docsAnchor: "facebook",
|
|
398
|
+
detect: (home) => {
|
|
399
|
+
const cfg = readJson(home, "facebook.json");
|
|
400
|
+
return cfg?.pageId ? { present: true, detail: cfg.pageName ?? cfg.pageId } : { present: false };
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
id: "instagram",
|
|
405
|
+
label: {
|
|
406
|
+
ko: "Instagram",
|
|
407
|
+
en: "Instagram"
|
|
408
|
+
},
|
|
409
|
+
requirement: "optional",
|
|
410
|
+
group: "marketing",
|
|
411
|
+
file: "~/.mimi-seed/instagram.json",
|
|
412
|
+
fix: "mimi-seed auth instagram",
|
|
413
|
+
setup: { kind: "mcp-bin", bin: "mimi-seed-social-auth", args: ["instagram"] },
|
|
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
|
+
},
|
|
428
|
+
docsAnchor: "instagram",
|
|
429
|
+
detect: (home) => {
|
|
430
|
+
const cfg = readJson(home, "instagram.json");
|
|
431
|
+
return cfg?.userId ? { present: true, detail: cfg.username ?? cfg.userId } : { present: false };
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
id: "anthropic",
|
|
436
|
+
label: {
|
|
437
|
+
ko: "ANTHROPIC_API_KEY",
|
|
438
|
+
en: "ANTHROPIC_API_KEY"
|
|
439
|
+
},
|
|
440
|
+
requirement: "optional",
|
|
441
|
+
group: "marketing",
|
|
442
|
+
fix: "export ANTHROPIC_API_KEY=sk-ant-\u2026",
|
|
443
|
+
setup: { kind: "env", envVar: "ANTHROPIC_API_KEY" },
|
|
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
|
+
},
|
|
456
|
+
docsAnchor: "anthropic-api-key",
|
|
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
|
+
},
|
|
461
|
+
detect: () => ({ present: Boolean(process.env.ANTHROPIC_API_KEY) })
|
|
462
|
+
}
|
|
463
|
+
];
|
|
464
|
+
function tryCredById(id) {
|
|
465
|
+
return CREDENTIALS.find((c) => c.id === id);
|
|
466
|
+
}
|
|
467
|
+
function detectAll(home = os2.homedir()) {
|
|
468
|
+
return new Map(CREDENTIALS.map((c) => [c.id, c.detect(home)]));
|
|
469
|
+
}
|
|
470
|
+
function isSatisfied(spec, detected) {
|
|
471
|
+
if (detected.get(spec.id)?.present) return true;
|
|
472
|
+
return (spec.fallbackOn ?? []).some((f) => detected.get(f)?.present);
|
|
473
|
+
}
|
|
474
|
+
function missingRequired(detected, platforms) {
|
|
475
|
+
return CREDENTIALS.filter((spec) => {
|
|
476
|
+
if (spec.requirement === "optional") return false;
|
|
477
|
+
if (spec.requirement === "platform" && !platforms.includes(spec.platform)) return false;
|
|
478
|
+
return !isSatisfied(spec, detected);
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
var GROUP_ORDER = { core: 0, ci: 1, marketing: 2 };
|
|
482
|
+
var REQ_ORDER = { required: 0, platform: 1, optional: 2 };
|
|
483
|
+
function planSetup(detected, opts = {}) {
|
|
484
|
+
const reconnect = new Set(opts.reconnect ?? []);
|
|
485
|
+
const only = opts.only ? new Set(opts.only) : null;
|
|
486
|
+
const platforms = opts.platforms ?? [];
|
|
487
|
+
return CREDENTIALS.filter((spec) => {
|
|
488
|
+
if (only && !only.has(spec.id)) return false;
|
|
489
|
+
if (reconnect.has(spec.id)) return true;
|
|
490
|
+
if (detected.get(spec.id)?.present) return false;
|
|
491
|
+
if (spec.requirement === "platform" && platforms.length > 0 && !platforms.includes(spec.platform)) {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
return true;
|
|
495
|
+
}).sort(
|
|
496
|
+
(a, b) => GROUP_ORDER[a.group] - GROUP_ORDER[b.group] || REQ_ORDER[a.requirement] - REQ_ORDER[b.requirement]
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// src/mcp-bin.ts
|
|
501
|
+
import { spawn, spawnSync } from "child_process";
|
|
502
|
+
var MCP_PKG = "@yoonion/mimi-seed-mcp";
|
|
503
|
+
function resolveOnPath(bin) {
|
|
504
|
+
if (process.env.MIMI_SEED_FORCE_NPX) return false;
|
|
505
|
+
const probe = process.platform === "win32" ? "where" : "which";
|
|
506
|
+
const r = spawnSync(probe, [bin], { stdio: "ignore", shell: true });
|
|
507
|
+
return r.status === 0;
|
|
508
|
+
}
|
|
509
|
+
async function runMcpBin(bin, extraArgs = []) {
|
|
510
|
+
const local = resolveOnPath(bin);
|
|
511
|
+
const cmd = local ? bin : "npx";
|
|
512
|
+
const args = local ? extraArgs : ["-y", MCP_PKG, bin, ...extraArgs];
|
|
513
|
+
return new Promise((resolve) => {
|
|
514
|
+
const child = spawn(cmd, args, {
|
|
515
|
+
stdio: "inherit",
|
|
516
|
+
shell: true,
|
|
517
|
+
env: { ...process.env, MIMI_SEED_LANG: resolveLang() }
|
|
518
|
+
});
|
|
519
|
+
child.on("error", (e) => {
|
|
520
|
+
process.stderr.write(`
|
|
521
|
+
\u274C ${cmd} \uC2E4\uD589 \uC2E4\uD328: ${e.message}
|
|
522
|
+
`);
|
|
523
|
+
resolve(1);
|
|
524
|
+
});
|
|
525
|
+
child.on("exit", (code) => resolve(code ?? 0));
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// src/jenkins-config.ts
|
|
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()) {
|
|
537
|
+
try {
|
|
538
|
+
const p = path3.join(home, ".mimi-seed", "jenkins.json");
|
|
539
|
+
return JSON.parse(fs3.readFileSync(p, "utf-8"));
|
|
540
|
+
} catch {
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
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;
|
|
549
|
+
let legacy;
|
|
550
|
+
try {
|
|
551
|
+
legacy = JSON.parse(fs3.readFileSync(legacyPath, "utf-8"));
|
|
552
|
+
} catch {
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
const j = legacy.jenkins;
|
|
556
|
+
if (!j?.url || !j?.token) return false;
|
|
557
|
+
const migrated = {
|
|
558
|
+
url: j.url,
|
|
559
|
+
username: j.username ?? j.user ?? "admin",
|
|
560
|
+
// 필드명 리네임: user → username
|
|
561
|
+
token: j.token,
|
|
562
|
+
...j.jobAndroid ? { jobAndroid: j.jobAndroid } : {},
|
|
563
|
+
...j.jobIos ? { jobIos: j.jobIos } : {}
|
|
564
|
+
};
|
|
565
|
+
fs3.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
566
|
+
fs3.writeFileSync(jenkinsPath, JSON.stringify(migrated, null, 2), { mode: 384 });
|
|
567
|
+
delete legacy.jenkins;
|
|
568
|
+
const tmp = `${legacyPath}.tmp`;
|
|
569
|
+
fs3.writeFileSync(tmp, JSON.stringify(legacy, null, 2), { mode: 384 });
|
|
570
|
+
fs3.renameSync(tmp, legacyPath);
|
|
571
|
+
return true;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/detect.ts
|
|
575
|
+
import fs4 from "fs/promises";
|
|
576
|
+
import path4 from "path";
|
|
577
|
+
async function readIfExists(p) {
|
|
578
|
+
try {
|
|
579
|
+
return await fs4.readFile(p, "utf8");
|
|
580
|
+
} catch {
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
async function pathExists(p) {
|
|
585
|
+
try {
|
|
586
|
+
await fs4.access(p);
|
|
587
|
+
return true;
|
|
588
|
+
} catch {
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
async function walk(root, match, maxDepth = 5) {
|
|
593
|
+
const found = [];
|
|
594
|
+
const skipDirs = /* @__PURE__ */ new Set([
|
|
595
|
+
"node_modules",
|
|
596
|
+
".git",
|
|
597
|
+
"build",
|
|
598
|
+
"dist",
|
|
599
|
+
".next",
|
|
600
|
+
".expo",
|
|
601
|
+
"Pods",
|
|
602
|
+
"DerivedData"
|
|
603
|
+
]);
|
|
604
|
+
async function visit(dir, depth) {
|
|
605
|
+
if (depth > maxDepth) return;
|
|
606
|
+
let entries;
|
|
607
|
+
try {
|
|
608
|
+
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
609
|
+
} catch {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
for (const e of entries) {
|
|
613
|
+
if (e.isDirectory()) {
|
|
614
|
+
if (skipDirs.has(e.name)) continue;
|
|
615
|
+
await visit(path4.join(dir, e.name), depth + 1);
|
|
616
|
+
} else if (e.isFile() && match(e.name)) {
|
|
617
|
+
found.push(path4.join(dir, e.name));
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
await visit(root, 0);
|
|
622
|
+
return found;
|
|
623
|
+
}
|
|
624
|
+
async function detectHints(cwd) {
|
|
625
|
+
const hints = [];
|
|
626
|
+
for (const fname of ["app.json", "app.config.json"]) {
|
|
627
|
+
const txt = await readIfExists(path4.join(cwd, fname));
|
|
628
|
+
if (!txt) continue;
|
|
629
|
+
try {
|
|
630
|
+
const json = JSON.parse(txt);
|
|
631
|
+
const expo = json.expo ?? json;
|
|
632
|
+
const pkg = expo?.android?.package;
|
|
633
|
+
const bid = expo?.ios?.bundleIdentifier;
|
|
634
|
+
const name = expo?.name;
|
|
635
|
+
if (pkg || bid) {
|
|
636
|
+
hints.push({
|
|
637
|
+
name,
|
|
638
|
+
packageName: typeof pkg === "string" ? pkg : void 0,
|
|
639
|
+
bundleId: typeof bid === "string" ? bid : void 0,
|
|
640
|
+
source: [fname]
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
} catch {
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
const gradleFiles = await walk(
|
|
647
|
+
cwd,
|
|
648
|
+
(n) => n === "build.gradle" || n === "build.gradle.kts",
|
|
649
|
+
4
|
|
650
|
+
);
|
|
651
|
+
for (const f of gradleFiles) {
|
|
652
|
+
const txt = await readIfExists(f);
|
|
653
|
+
if (!txt) continue;
|
|
654
|
+
const m = txt.match(/applicationId[\s=]+["']([^"']+)["']/);
|
|
655
|
+
if (m?.[1]) {
|
|
656
|
+
const pkg = m[1];
|
|
657
|
+
if (!hints.some((h) => h.packageName === pkg)) {
|
|
658
|
+
hints.push({ packageName: pkg, source: [path4.relative(cwd, f)] });
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
const plistFiles = await walk(cwd, (n) => n === "Info.plist", 5);
|
|
663
|
+
for (const f of plistFiles) {
|
|
664
|
+
const txt = await readIfExists(f);
|
|
665
|
+
if (!txt) continue;
|
|
666
|
+
const m = txt.match(
|
|
667
|
+
/<key>CFBundleIdentifier<\/key>\s*<string>([^<]+)<\/string>/
|
|
668
|
+
);
|
|
669
|
+
if (m?.[1]) {
|
|
670
|
+
let bid = m[1];
|
|
671
|
+
if (bid.includes("$(PRODUCT_BUNDLE_IDENTIFIER)")) continue;
|
|
672
|
+
if (!hints.some((h) => h.bundleId === bid)) {
|
|
673
|
+
hints.push({ bundleId: bid, source: [path4.relative(cwd, f)] });
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
const pbxFiles = await walk(cwd, (n) => n === "project.pbxproj", 5);
|
|
678
|
+
for (const f of pbxFiles) {
|
|
679
|
+
const txt = await readIfExists(f);
|
|
680
|
+
if (!txt) continue;
|
|
681
|
+
const matches = [...txt.matchAll(/PRODUCT_BUNDLE_IDENTIFIER = ([^;]+);/g)];
|
|
682
|
+
for (const m of matches) {
|
|
683
|
+
const bid = m[1].trim().replace(/^["']|["']$/g, "");
|
|
684
|
+
if (!bid || bid.includes("$")) continue;
|
|
685
|
+
if (!hints.some((h) => h.bundleId === bid)) {
|
|
686
|
+
hints.push({ bundleId: bid, source: [path4.relative(cwd, f)] });
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
const pkgJson = await readIfExists(path4.join(cwd, "package.json"));
|
|
691
|
+
if (pkgJson) {
|
|
692
|
+
try {
|
|
693
|
+
const json = JSON.parse(pkgJson);
|
|
694
|
+
if (typeof json.name === "string") {
|
|
695
|
+
for (const h of hints) if (!h.name) h.name = json.name;
|
|
696
|
+
}
|
|
697
|
+
} catch {
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
const merged = [];
|
|
701
|
+
const androidOnly = hints.filter((h) => h.packageName && !h.bundleId);
|
|
702
|
+
const iosOnly = hints.filter((h) => h.bundleId && !h.packageName);
|
|
703
|
+
const both = hints.filter((h) => h.packageName && h.bundleId);
|
|
704
|
+
merged.push(...both);
|
|
705
|
+
for (const a of androidOnly) {
|
|
706
|
+
const match = iosOnly.find((i) => i.bundleId === a.packageName);
|
|
707
|
+
if (match) {
|
|
708
|
+
merged.push({
|
|
709
|
+
name: a.name ?? match.name,
|
|
710
|
+
packageName: a.packageName,
|
|
711
|
+
bundleId: match.bundleId,
|
|
712
|
+
source: [...a.source, ...match.source]
|
|
713
|
+
});
|
|
714
|
+
} else {
|
|
715
|
+
merged.push(a);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
for (const i of iosOnly) {
|
|
719
|
+
if (!androidOnly.some((a) => a.packageName === i.bundleId)) {
|
|
720
|
+
merged.push(i);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return merged.filter((h) => h.packageName || h.bundleId);
|
|
724
|
+
}
|
|
725
|
+
async function hasAnyProjectSignal(cwd) {
|
|
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"));
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// src/deploy.ts
|
|
730
|
+
import kleur from "kleur";
|
|
731
|
+
import * as readline from "readline";
|
|
732
|
+
|
|
733
|
+
// src/config.ts
|
|
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");
|
|
739
|
+
async function readConfig() {
|
|
740
|
+
try {
|
|
741
|
+
const txt = await fs5.readFile(CONFIG_PATH, "utf8");
|
|
742
|
+
return JSON.parse(txt);
|
|
743
|
+
} catch {
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
async function writeConfig(cfg) {
|
|
748
|
+
await fs5.mkdir(CONFIG_DIR2, { recursive: true });
|
|
749
|
+
await fs5.writeFile(CONFIG_PATH, JSON.stringify(cfg, null, 2));
|
|
750
|
+
if (process.platform !== "win32") {
|
|
751
|
+
await fs5.chmod(CONFIG_PATH, 384);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
async function deleteConfig() {
|
|
755
|
+
await fs5.rm(CONFIG_PATH, { force: true });
|
|
756
|
+
}
|
|
757
|
+
var CONFIG_LOCATION = CONFIG_PATH;
|
|
758
|
+
async function getEffectiveConfig() {
|
|
759
|
+
const envToken = process.env.MIMI_SEED_TOKEN;
|
|
760
|
+
if (envToken) {
|
|
761
|
+
const webBase = process.env.MIMI_SEED_WEB_BASE ?? "https://mimi-seed.pryzm.gg";
|
|
762
|
+
return {
|
|
763
|
+
token: envToken,
|
|
764
|
+
prefix: envToken.slice(0, 8),
|
|
765
|
+
endpoint: `${webBase}/api/mcp`,
|
|
766
|
+
webBase,
|
|
767
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
return readConfig();
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// src/ci-providers.ts
|
|
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");
|
|
778
|
+
function loadCiProviderConfig() {
|
|
779
|
+
try {
|
|
780
|
+
const cfg = JSON.parse(fs6.readFileSync(CI_CONFIG_PATH, "utf-8"));
|
|
781
|
+
const host = normalizeHost(cfg.host);
|
|
782
|
+
return host ? { ...cfg, host } : { ...cfg, host: void 0 };
|
|
783
|
+
} catch {
|
|
784
|
+
return null;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
function normalizeHost(host) {
|
|
788
|
+
const h = host?.trim();
|
|
789
|
+
if (!h) return void 0;
|
|
790
|
+
const withScheme = /^https?:\/\//i.test(h) ? h : `https://${h}`;
|
|
791
|
+
return withScheme.replace(/\/+$/, "");
|
|
792
|
+
}
|
|
793
|
+
function saveCiProviderConfig(cfg) {
|
|
794
|
+
const dir = path6.dirname(CI_CONFIG_PATH);
|
|
795
|
+
if (!fs6.existsSync(dir)) {
|
|
796
|
+
fs6.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
797
|
+
}
|
|
798
|
+
const normalized = { ...cfg, host: normalizeHost(cfg.host) };
|
|
799
|
+
if (!normalized.host) delete normalized.host;
|
|
800
|
+
fs6.writeFileSync(CI_CONFIG_PATH, JSON.stringify(normalized, null, 2));
|
|
801
|
+
if (process.platform !== "win32") {
|
|
802
|
+
fs6.chmodSync(CI_CONFIG_PATH, 384);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
async function verifyCiToken(cfg) {
|
|
806
|
+
const probe = { ...cfg, host: normalizeHost(cfg.host) };
|
|
807
|
+
try {
|
|
808
|
+
if (probe.provider === "github") {
|
|
809
|
+
const res2 = await fetch(`${ghBase(probe)}/user`, {
|
|
810
|
+
headers: { Authorization: `Bearer ${probe.token}`, Accept: "application/vnd.github+json" }
|
|
811
|
+
});
|
|
812
|
+
if (!res2.ok) {
|
|
813
|
+
return { ok: false, reason: `GitHub ${res2.status} \u2014 \uD1A0\uD070\uC774 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC544` };
|
|
814
|
+
}
|
|
815
|
+
const user2 = await res2.json();
|
|
816
|
+
const scopes = res2.headers.get("x-oauth-scopes");
|
|
817
|
+
if (scopes !== null && !scopes.split(/,\s*/).filter(Boolean).includes("workflow")) {
|
|
818
|
+
return {
|
|
819
|
+
ok: false,
|
|
820
|
+
reason: `\uD1A0\uD070\uC5D0 \`workflow\` \uC2A4\uCF54\uD504\uAC00 \uC5C6\uC5B4 (\uD604\uC7AC: ${scopes || "\uC5C6\uC74C"}). \uC6CC\uD06C\uD50C\uB85C \uC2E4\uD589\uC774 403 \uC73C\uB85C \uB9C9\uD78C\uB2E4.`
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
return { ok: true, login: user2.login };
|
|
824
|
+
}
|
|
825
|
+
const res = await fetch(`${glBase(probe)}/user`, { headers: { "PRIVATE-TOKEN": probe.token } });
|
|
826
|
+
if (!res.ok) {
|
|
827
|
+
return { ok: false, reason: `GitLab ${res.status} \u2014 \uD1A0\uD070\uC774 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC544` };
|
|
828
|
+
}
|
|
829
|
+
const user = await res.json();
|
|
830
|
+
return { ok: true, login: user.username };
|
|
831
|
+
} catch (e) {
|
|
832
|
+
return { ok: false, reason: e instanceof Error ? e.message : String(e) };
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
function ghBase(cfg) {
|
|
836
|
+
if (cfg.host) return `${cfg.host.replace(/\/$/, "")}/api/v3`;
|
|
837
|
+
return "https://api.github.com";
|
|
838
|
+
}
|
|
839
|
+
function ghHeaders(token) {
|
|
840
|
+
return {
|
|
841
|
+
Authorization: `Bearer ${token}`,
|
|
842
|
+
Accept: "application/vnd.github+json",
|
|
843
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
844
|
+
"Content-Type": "application/json"
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
async function ghTriggerWorkflow(cfg, workflow, ref, inputs = {}) {
|
|
848
|
+
const startTime = /* @__PURE__ */ new Date();
|
|
849
|
+
const wfId = /^\d+$/.test(workflow) ? Number(workflow) : workflow;
|
|
850
|
+
const dispatchRes = await fetch(
|
|
851
|
+
`${ghBase(cfg)}/repos/${cfg.owner}/${cfg.repo}/actions/workflows/${wfId}/dispatches`,
|
|
852
|
+
{
|
|
853
|
+
method: "POST",
|
|
854
|
+
headers: ghHeaders(cfg.token),
|
|
855
|
+
body: JSON.stringify({ ref, inputs })
|
|
856
|
+
}
|
|
857
|
+
);
|
|
858
|
+
if (!dispatchRes.ok) {
|
|
859
|
+
throw new Error(`GitHub dispatch ${dispatchRes.status}: ${await dispatchRes.text()}`);
|
|
860
|
+
}
|
|
861
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
862
|
+
const runsRes = await fetch(
|
|
863
|
+
`${ghBase(cfg)}/repos/${cfg.owner}/${cfg.repo}/actions/workflows/${wfId}/runs?per_page=5`,
|
|
864
|
+
{ headers: ghHeaders(cfg.token) }
|
|
865
|
+
);
|
|
866
|
+
if (!runsRes.ok) return null;
|
|
867
|
+
const data = await runsRes.json();
|
|
868
|
+
const run = data.workflow_runs.find((r) => new Date(r.created_at) >= startTime) ?? data.workflow_runs[0];
|
|
869
|
+
if (!run) return null;
|
|
870
|
+
return { runId: run.id, url: run.html_url };
|
|
871
|
+
}
|
|
872
|
+
async function ghPollRun(cfg, runId, onTick, timeoutMs = 30 * 60 * 1e3, intervalMs = 15e3) {
|
|
873
|
+
const start = Date.now();
|
|
874
|
+
let consecutiveErrors = 0;
|
|
875
|
+
while (Date.now() - start < timeoutMs) {
|
|
876
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
877
|
+
let res;
|
|
878
|
+
try {
|
|
879
|
+
res = await fetch(
|
|
880
|
+
`${ghBase(cfg)}/repos/${cfg.owner}/${cfg.repo}/actions/runs/${runId}`,
|
|
881
|
+
{ headers: ghHeaders(cfg.token) }
|
|
882
|
+
);
|
|
883
|
+
} catch {
|
|
884
|
+
consecutiveErrors++;
|
|
885
|
+
if (consecutiveErrors >= 3) throw new Error("GitHub API \uC5F0\uC18D \uC624\uB958 3\uD68C");
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
if (!res.ok) {
|
|
889
|
+
consecutiveErrors++;
|
|
890
|
+
if (consecutiveErrors >= 3) throw new Error(`GitHub API \uC5F0\uC18D \uC624\uB958 (HTTP ${res.status})`);
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
consecutiveErrors = 0;
|
|
894
|
+
const data = await res.json();
|
|
895
|
+
onTick?.(data.status);
|
|
896
|
+
if (data.status === "completed") {
|
|
897
|
+
if (data.conclusion === "success") return "success";
|
|
898
|
+
if (data.conclusion === "cancelled") return "cancelled";
|
|
899
|
+
return "failure";
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return "timeout";
|
|
903
|
+
}
|
|
904
|
+
function glBase(cfg) {
|
|
905
|
+
return `${cfg.host ?? "https://gitlab.com"}/api/v4`;
|
|
906
|
+
}
|
|
907
|
+
function glProjectId(cfg) {
|
|
908
|
+
return encodeURIComponent(`${cfg.owner}/${cfg.repo}`);
|
|
909
|
+
}
|
|
910
|
+
async function glTriggerPipeline(cfg, ref, variables = {}) {
|
|
911
|
+
const vars = Object.entries(variables).map(([key, value]) => ({ key, value }));
|
|
912
|
+
const body = { ref };
|
|
913
|
+
if (vars.length > 0) body.variables = vars;
|
|
914
|
+
const res = await fetch(`${glBase(cfg)}/projects/${glProjectId(cfg)}/pipeline`, {
|
|
915
|
+
method: "POST",
|
|
916
|
+
headers: { "PRIVATE-TOKEN": cfg.token, "Content-Type": "application/json" },
|
|
917
|
+
body: JSON.stringify(body)
|
|
918
|
+
});
|
|
919
|
+
if (!res.ok) throw new Error(`GitLab trigger ${res.status}: ${await res.text()}`);
|
|
920
|
+
const data = await res.json();
|
|
921
|
+
return { pipelineId: data.id, url: data.web_url };
|
|
922
|
+
}
|
|
923
|
+
async function glPollPipeline(cfg, pipelineId, onTick, timeoutMs = 30 * 60 * 1e3, intervalMs = 15e3) {
|
|
924
|
+
const start = Date.now();
|
|
925
|
+
let consecutiveErrors = 0;
|
|
926
|
+
while (Date.now() - start < timeoutMs) {
|
|
927
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
928
|
+
let res;
|
|
929
|
+
try {
|
|
930
|
+
res = await fetch(
|
|
931
|
+
`${glBase(cfg)}/projects/${glProjectId(cfg)}/pipelines/${pipelineId}`,
|
|
932
|
+
{ headers: { "PRIVATE-TOKEN": cfg.token } }
|
|
933
|
+
);
|
|
934
|
+
} catch {
|
|
935
|
+
consecutiveErrors++;
|
|
936
|
+
if (consecutiveErrors >= 3) throw new Error("GitLab API \uC5F0\uC18D \uC624\uB958 3\uD68C");
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
if (!res.ok) {
|
|
940
|
+
consecutiveErrors++;
|
|
941
|
+
if (consecutiveErrors >= 3) throw new Error(`GitLab API \uC5F0\uC18D \uC624\uB958 (HTTP ${res.status})`);
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
consecutiveErrors = 0;
|
|
945
|
+
const data = await res.json();
|
|
946
|
+
onTick?.(data.status);
|
|
947
|
+
if (data.status === "success") return "success";
|
|
948
|
+
if (data.status === "failed") return "failure";
|
|
949
|
+
if (data.status === "canceled" || data.status === "skipped") return "cancelled";
|
|
950
|
+
}
|
|
951
|
+
return "timeout";
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// src/deploy.ts
|
|
955
|
+
var PHASE_ICON = {
|
|
956
|
+
init: "\u{1F680}",
|
|
957
|
+
verify: "\u{1F50D}",
|
|
958
|
+
notes: "\u{1F4DD}",
|
|
959
|
+
apply: "\u{1F4E4}",
|
|
960
|
+
promote: "\u{1F3AF}",
|
|
961
|
+
done: "\u{1F389}",
|
|
962
|
+
error: "\u274C"
|
|
963
|
+
};
|
|
964
|
+
function log(msg) {
|
|
965
|
+
process.stdout.write(msg + "\n");
|
|
966
|
+
}
|
|
967
|
+
function jenkinsHeaders(cfg) {
|
|
968
|
+
const creds = Buffer.from(`${cfg.username || "admin"}:${cfg.token}`).toString("base64");
|
|
969
|
+
return { Authorization: `Basic ${creds}`, "Content-Type": "application/json" };
|
|
970
|
+
}
|
|
971
|
+
async function triggerBuild(cfg, jobName, params) {
|
|
972
|
+
const qs = new URLSearchParams(params).toString();
|
|
973
|
+
const url = `${cfg.url}/job/${encodeURIComponent(jobName)}/buildWithParameters?${qs}`;
|
|
974
|
+
const res = await fetch(url, { method: "POST", headers: jenkinsHeaders(cfg) });
|
|
975
|
+
if (!res.ok) {
|
|
976
|
+
throw new Error(`Jenkins \uD2B8\uB9AC\uAC70 \uC2E4\uD328 ${res.status}: ${await res.text()}`);
|
|
977
|
+
}
|
|
978
|
+
const location = res.headers.get("Location") ?? "";
|
|
979
|
+
const match = location.match(/\/queue\/item\/(\d+)\//);
|
|
980
|
+
return match?.[1] ?? "";
|
|
981
|
+
}
|
|
982
|
+
async function getQueueBuildNumber(cfg, queueItemId) {
|
|
983
|
+
const url = `${cfg.url}/queue/item/${queueItemId}/api/json`;
|
|
984
|
+
const res = await fetch(url, { headers: jenkinsHeaders(cfg) });
|
|
985
|
+
if (!res.ok) return null;
|
|
986
|
+
const data = await res.json();
|
|
987
|
+
return data.executable?.number ?? null;
|
|
988
|
+
}
|
|
989
|
+
async function getBuildStatus(cfg, jobName, buildNumber) {
|
|
990
|
+
const url = `${cfg.url}/job/${encodeURIComponent(jobName)}/${buildNumber}/api/json`;
|
|
991
|
+
const res = await fetch(url, { headers: jenkinsHeaders(cfg) });
|
|
992
|
+
if (!res.ok) throw new Error(`\uBE4C\uB4DC \uC0C1\uD0DC \uC870\uD68C \uC2E4\uD328 ${res.status}`);
|
|
993
|
+
const data = await res.json();
|
|
994
|
+
return {
|
|
995
|
+
building: data.building ?? true,
|
|
996
|
+
result: data.result ?? null,
|
|
997
|
+
duration: data.duration ?? 0
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
async function pollBuildComplete(cfg, jobName, buildNumber, timeoutMs = 30 * 60 * 1e3) {
|
|
1001
|
+
const start = Date.now();
|
|
1002
|
+
const intervalMs = 15e3;
|
|
1003
|
+
let dots = 0;
|
|
1004
|
+
let consecutiveErrors = 0;
|
|
1005
|
+
while (Date.now() - start < timeoutMs) {
|
|
1006
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
1007
|
+
let status;
|
|
1008
|
+
try {
|
|
1009
|
+
status = await getBuildStatus(cfg, jobName, buildNumber);
|
|
1010
|
+
consecutiveErrors = 0;
|
|
1011
|
+
} catch {
|
|
1012
|
+
consecutiveErrors++;
|
|
1013
|
+
if (consecutiveErrors >= 3) throw new Error("Jenkins \uC5F0\uACB0 \uC624\uB958 3\uD68C \uC5F0\uC18D \u2014 \uB124\uD2B8\uC6CC\uD06C\uB97C \uD655\uC778\uD558\uC138\uC694");
|
|
1014
|
+
process.stdout.write(`\r \u26A0 Jenkins \uC5F0\uACB0 \uC624\uB958 (${consecutiveErrors}/3\uD68C), \uC7AC\uC2DC\uB3C4... `);
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
dots = (dots + 1) % 4;
|
|
1018
|
+
process.stdout.write(`\r \u23F3 \uBE4C\uB4DC \uC9C4\uD589 \uC911${".".repeat(dots + 1)} `);
|
|
1019
|
+
if (!status.building) {
|
|
1020
|
+
process.stdout.write("\n");
|
|
1021
|
+
return status.result ?? "FAILURE";
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
throw new Error("\uBE4C\uB4DC \uD0C0\uC784\uC544\uC6C3 (30\uBD84)");
|
|
1025
|
+
}
|
|
1026
|
+
async function streamDeploy(webBase, token, body) {
|
|
1027
|
+
const res = await fetch(`${webBase}/api/deploy`, {
|
|
1028
|
+
method: "POST",
|
|
1029
|
+
headers: {
|
|
1030
|
+
"Content-Type": "application/json",
|
|
1031
|
+
Authorization: `Bearer ${token}`
|
|
1032
|
+
},
|
|
1033
|
+
body: JSON.stringify(body)
|
|
1034
|
+
});
|
|
1035
|
+
if (!res.ok) {
|
|
1036
|
+
const text = await res.text().catch(() => "");
|
|
1037
|
+
throw new Error(`\uC11C\uBC84 \uBC30\uD3EC \uC2E4\uD328 ${res.status}: ${text.slice(0, 200)}`);
|
|
1038
|
+
}
|
|
1039
|
+
const reader = res.body?.getReader();
|
|
1040
|
+
if (!reader) throw new Error("SSE \uC2A4\uD2B8\uB9BC \uC5C6\uC74C");
|
|
1041
|
+
const decoder = new TextDecoder();
|
|
1042
|
+
let buf = "";
|
|
1043
|
+
while (true) {
|
|
1044
|
+
const { done, value } = await reader.read();
|
|
1045
|
+
if (done) break;
|
|
1046
|
+
buf += decoder.decode(value, { stream: true });
|
|
1047
|
+
const lines = buf.split("\n");
|
|
1048
|
+
buf = lines.pop() ?? "";
|
|
1049
|
+
for (const line of lines) {
|
|
1050
|
+
if (!line.startsWith("data: ")) continue;
|
|
1051
|
+
try {
|
|
1052
|
+
const event = JSON.parse(line.slice(6));
|
|
1053
|
+
const icon = PHASE_ICON[event.phase] ?? "\u25B8";
|
|
1054
|
+
const color = event.status === "done" ? kleur.green : event.status === "failed" ? kleur.red : event.status === "skipped" ? kleur.yellow : kleur.dim;
|
|
1055
|
+
log(` ${icon} ${color(event.message)}`);
|
|
1056
|
+
if (event.phase === "error" || event.phase === "verify" && event.status === "failed" || event.phase === "promote" && event.status === "failed") {
|
|
1057
|
+
process.exit(1);
|
|
1058
|
+
}
|
|
1059
|
+
} catch {
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
function parseArgs(argv) {
|
|
1065
|
+
const args = {
|
|
1066
|
+
platform: "android",
|
|
1067
|
+
language: "ko-KR",
|
|
1068
|
+
dryRun: false,
|
|
1069
|
+
yes: false,
|
|
1070
|
+
skipBuild: false,
|
|
1071
|
+
setupJenkins: false,
|
|
1072
|
+
setupGithub: false,
|
|
1073
|
+
setupGitlab: false,
|
|
1074
|
+
ci: "auto",
|
|
1075
|
+
ref: "main"
|
|
1076
|
+
};
|
|
1077
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1078
|
+
if ((argv[i] === "--platform" || argv[i] === "-p") && argv[i + 1]) args.platform = argv[++i];
|
|
1079
|
+
if (argv[i] === "--app" && argv[i + 1]) args.appId = argv[++i];
|
|
1080
|
+
if (argv[i] === "--version-code" && argv[i + 1]) args.versionCode = Number(argv[++i]);
|
|
1081
|
+
if (argv[i] === "--from" && argv[i + 1]) args.fromRef = argv[++i];
|
|
1082
|
+
if (argv[i] === "--to" && argv[i + 1]) args.toRef = argv[++i];
|
|
1083
|
+
if (argv[i] === "--language" && argv[i + 1]) args.language = argv[++i];
|
|
1084
|
+
if (argv[i] === "--dry-run") args.dryRun = true;
|
|
1085
|
+
if (argv[i] === "--yes" || argv[i] === "-y") args.yes = true;
|
|
1086
|
+
if (argv[i] === "--skip-build") args.skipBuild = true;
|
|
1087
|
+
if (argv[i] === "--ci" && argv[i + 1]) args.ci = argv[++i];
|
|
1088
|
+
if (argv[i] === "--workflow" && argv[i + 1]) args.workflow = argv[++i];
|
|
1089
|
+
if (argv[i] === "--ref" && argv[i + 1]) args.ref = argv[++i];
|
|
1090
|
+
if (argv[i] === "setup-jenkins") args.setupJenkins = true;
|
|
1091
|
+
if (argv[i] === "setup-github") args.setupGithub = true;
|
|
1092
|
+
if (argv[i] === "setup-gitlab") args.setupGitlab = true;
|
|
1093
|
+
}
|
|
1094
|
+
return args;
|
|
1095
|
+
}
|
|
1096
|
+
async function promptGitProviderSetup(provider) {
|
|
1097
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1098
|
+
const ask2 = (q) => new Promise((resolve) => rl.question(q, (a) => resolve(a.trim())));
|
|
1099
|
+
const isGh = provider === "github";
|
|
1100
|
+
log(kleur.bold(isGh ? "GitHub Actions \uC124\uC815" : "GitLab CI \uC124\uC815"));
|
|
1101
|
+
const tokenLabel = isGh ? " GitHub Personal Access Token (repo+workflow \uC2A4\uCF54\uD504): " : " GitLab Personal Access Token: ";
|
|
1102
|
+
const token = await ask2(tokenLabel);
|
|
1103
|
+
const owner = await ask2(isGh ? " Owner (org/user): " : " Namespace/group: ");
|
|
1104
|
+
const repo = await ask2(" Repo \uC774\uB984 (\uACBD\uB85C \uC5C6\uC774): ");
|
|
1105
|
+
const hostPrompt = isGh ? " GitHub Enterprise host (\uC120\uD0DD, \uC5D4\uD130=github.com): " : " GitLab self-hosted URL (\uC120\uD0DD, \uC5D4\uD130=gitlab.com): ";
|
|
1106
|
+
const host = await ask2(hostPrompt);
|
|
1107
|
+
rl.close();
|
|
1108
|
+
return {
|
|
1109
|
+
provider,
|
|
1110
|
+
token,
|
|
1111
|
+
owner,
|
|
1112
|
+
repo,
|
|
1113
|
+
host: host || void 0
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
function resolveCi(ciOption, jenkins, ciProvider) {
|
|
1117
|
+
if (ciOption !== "auto") return ciOption;
|
|
1118
|
+
if (jenkins?.url && jenkins.token) return "jenkins";
|
|
1119
|
+
if (ciProvider) return ciProvider.provider;
|
|
1120
|
+
throw new Error(
|
|
1121
|
+
"CI \uC124\uC815 \uC5C6\uC74C. \uB2E4\uC74C \uC911 \uD558\uB098 \uC2E4\uD589:\n \u2022 mimi-seed deploy setup-jenkins\n \u2022 mimi-seed deploy setup-github\n \u2022 mimi-seed deploy setup-gitlab"
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
async function runGitProviderBuild(cfg, args) {
|
|
1125
|
+
let runUrl = "";
|
|
1126
|
+
let runId;
|
|
1127
|
+
if (cfg.provider === "github") {
|
|
1128
|
+
if (!args.workflow) {
|
|
1129
|
+
throw new Error("--workflow \uD544\uC694 (\uC608: --workflow deploy.yml)");
|
|
1130
|
+
}
|
|
1131
|
+
log(`\u{1F528} GitHub Actions \uD2B8\uB9AC\uAC70: ${kleur.cyan(args.workflow)} @ ${args.ref}`);
|
|
1132
|
+
const inputs = {};
|
|
1133
|
+
if (args.appId) inputs.MIMI_APP_ID = args.appId;
|
|
1134
|
+
inputs.PLATFORM = args.platform;
|
|
1135
|
+
const result2 = await ghTriggerWorkflow(cfg, args.workflow, args.ref, inputs);
|
|
1136
|
+
if (!result2) {
|
|
1137
|
+
throw new Error("GitHub Actions run_id \uC870\uD68C \uC2E4\uD328. \uC7A0\uC2DC \uD6C4 ci_list_recent_builds \uB85C \uD655\uC778\uD558\uC138\uC694.");
|
|
1138
|
+
}
|
|
1139
|
+
runId = result2.runId;
|
|
1140
|
+
runUrl = result2.url;
|
|
1141
|
+
log(kleur.dim(` Run ID: ${runId} \u2192 ${runUrl}`));
|
|
1142
|
+
} else {
|
|
1143
|
+
log(`\u{1F528} GitLab Pipeline \uD2B8\uB9AC\uAC70: ${args.ref}`);
|
|
1144
|
+
const variables = { PLATFORM: args.platform };
|
|
1145
|
+
if (args.appId) variables.MIMI_APP_ID = args.appId;
|
|
1146
|
+
const result2 = await glTriggerPipeline(cfg, args.ref, variables);
|
|
1147
|
+
runId = result2.pipelineId;
|
|
1148
|
+
runUrl = result2.url;
|
|
1149
|
+
log(kleur.dim(` Pipeline ID: ${runId} \u2192 ${runUrl}`));
|
|
1150
|
+
}
|
|
1151
|
+
log(" \uC644\uB8CC \uB300\uAE30 \uC911...");
|
|
1152
|
+
let dots = 0;
|
|
1153
|
+
const onTick = (status) => {
|
|
1154
|
+
dots = (dots + 1) % 4;
|
|
1155
|
+
process.stdout.write(`\r \u23F3 ${status}${".".repeat(dots + 1)} `);
|
|
1156
|
+
};
|
|
1157
|
+
const result = cfg.provider === "github" ? await ghPollRun(cfg, runId, onTick) : await glPollPipeline(cfg, runId, onTick);
|
|
1158
|
+
process.stdout.write("\n");
|
|
1159
|
+
if (result === "success") {
|
|
1160
|
+
log(kleur.green(`\u2705 \uBE4C\uB4DC #${runId} \uC131\uACF5`));
|
|
1161
|
+
return runId;
|
|
1162
|
+
}
|
|
1163
|
+
log(kleur.red(`\uBE4C\uB4DC \uC885\uB8CC: ${result}`));
|
|
1164
|
+
log(kleur.dim(` ${runUrl}`));
|
|
1165
|
+
log(kleur.dim(` \uBE4C\uB4DC\uAC00 \uC774\uBBF8 \uC644\uB8CC\uB410\uB2E4\uBA74: mimi-seed deploy --skip-build --version-code <N> --platform ${args.platform}`));
|
|
1166
|
+
process.exit(1);
|
|
1167
|
+
}
|
|
1168
|
+
async function cmdDeploy(argv) {
|
|
1169
|
+
const args = parseArgs(argv);
|
|
1170
|
+
const cfg = await getEffectiveConfig();
|
|
1171
|
+
if (!cfg) {
|
|
1172
|
+
log(kleur.red("\uC5F0\uACB0\uB41C \uACC4\uC815 \uC5C6\uC74C. `mimi-seed init` \uC2E4\uD589."));
|
|
1173
|
+
process.exit(1);
|
|
1174
|
+
}
|
|
1175
|
+
if (args.setupJenkins) {
|
|
1176
|
+
const code = await runMcpBin("mimi-seed-jenkins-auth");
|
|
1177
|
+
if (code !== 0) process.exit(code);
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
if (args.setupGithub) {
|
|
1181
|
+
const ciCfg = await promptGitProviderSetup("github");
|
|
1182
|
+
saveCiProviderConfig(ciCfg);
|
|
1183
|
+
log(kleur.green(`\u2705 GitHub Actions \uC124\uC815 \uC800\uC7A5\uB428 \u2192 ~/.mimi-seed/ci.json`));
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
if (args.setupGitlab) {
|
|
1187
|
+
const ciCfg = await promptGitProviderSetup("gitlab");
|
|
1188
|
+
saveCiProviderConfig(ciCfg);
|
|
1189
|
+
log(kleur.green(`\u2705 GitLab CI \uC124\uC815 \uC800\uC7A5\uB428 \u2192 ~/.mimi-seed/ci.json`));
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
log(kleur.bold(`mimi-seed deploy \u2014 ${args.platform}`));
|
|
1193
|
+
if (args.dryRun) log(kleur.yellow(" [dry-run \uBAA8\uB4DC] \uC2E4\uC81C \uBC30\uD3EC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4"));
|
|
1194
|
+
log("");
|
|
1195
|
+
let versionCode = args.versionCode;
|
|
1196
|
+
if (!args.skipBuild) {
|
|
1197
|
+
const ciProvider = loadCiProviderConfig();
|
|
1198
|
+
migrateLegacyJenkins();
|
|
1199
|
+
const jenkinsCfg = loadJenkinsConfig() ?? void 0;
|
|
1200
|
+
const kind = resolveCi(args.ci, jenkinsCfg, ciProvider);
|
|
1201
|
+
log(kleur.dim(` CI: ${kind}`));
|
|
1202
|
+
if (kind === "jenkins") {
|
|
1203
|
+
if (!jenkinsCfg?.url || !jenkinsCfg?.token) {
|
|
1204
|
+
log(kleur.yellow("Jenkins \uC124\uC815 \uC5C6\uC74C. `mimi-seed deploy setup-jenkins` \uB85C \uC124\uC815\uD558\uAC70\uB098 --skip-build \uC0AC\uC6A9."));
|
|
1205
|
+
process.exit(1);
|
|
1206
|
+
}
|
|
1207
|
+
const jenkins = jenkinsCfg;
|
|
1208
|
+
const jobName = args.platform === "android" ? jenkins.jobAndroid : jenkins.jobIos;
|
|
1209
|
+
if (!jobName) {
|
|
1210
|
+
log(kleur.red(`${args.platform} Jenkins job\uC774 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. setup-jenkins \uC2E4\uD589.`));
|
|
1211
|
+
process.exit(1);
|
|
1212
|
+
}
|
|
1213
|
+
log(`\u{1F528} Jenkins \uBE4C\uB4DC \uD2B8\uB9AC\uAC70: ${kleur.cyan(jobName)}`);
|
|
1214
|
+
const buildParams = {};
|
|
1215
|
+
if (args.appId) buildParams.MIMI_APP_ID = args.appId;
|
|
1216
|
+
const queueItemId = await triggerBuild(jenkins, jobName, buildParams);
|
|
1217
|
+
if (!queueItemId) {
|
|
1218
|
+
log(kleur.yellow(" \u26A0 Queue item ID\uB97C \uAC00\uC838\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \uBE4C\uB4DC\uB294 \uC2DC\uC791\uB410\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4."));
|
|
1219
|
+
} else {
|
|
1220
|
+
log(kleur.dim(` Queue item: ${queueItemId}`));
|
|
1221
|
+
}
|
|
1222
|
+
let buildNumber = null;
|
|
1223
|
+
if (queueItemId) {
|
|
1224
|
+
log(" \uBE4C\uB4DC \uBC88\uD638 \uB300\uAE30 \uC911...");
|
|
1225
|
+
for (let i = 0; i < 6; i++) {
|
|
1226
|
+
await new Promise((r) => setTimeout(r, 5e3));
|
|
1227
|
+
buildNumber = await getQueueBuildNumber(jenkins, queueItemId).catch(() => null);
|
|
1228
|
+
if (buildNumber) break;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
if (!buildNumber) {
|
|
1232
|
+
log(kleur.yellow(" \uBE4C\uB4DC \uBC88\uD638\uB97C \uAC00\uC838\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. --skip-build + --version-code \uB85C \uC7AC\uC2DC\uB3C4 \uAC00\uB2A5."));
|
|
1233
|
+
process.exit(1);
|
|
1234
|
+
}
|
|
1235
|
+
log(` \uBE4C\uB4DC #${buildNumber} \uC2DC\uC791\uB428. \uC644\uB8CC \uB300\uAE30 \uC911...`);
|
|
1236
|
+
const result = await pollBuildComplete(jenkins, jobName, buildNumber);
|
|
1237
|
+
if (result !== "SUCCESS") {
|
|
1238
|
+
log(kleur.red(`\uBE4C\uB4DC \uC2E4\uD328: ${result}`));
|
|
1239
|
+
log(kleur.dim(` Jenkins: ${jenkins.url}/job/${encodeURIComponent(jobName)}/${buildNumber}/`));
|
|
1240
|
+
log(kleur.dim(` \uBE4C\uB4DC\uAC00 \uC774\uBBF8 \uC644\uB8CC\uB410\uB2E4\uBA74: mimi-seed deploy --skip-build --version-code ${buildNumber} --platform ${args.platform}`));
|
|
1241
|
+
process.exit(1);
|
|
1242
|
+
}
|
|
1243
|
+
log(kleur.green(`\u2705 \uBE4C\uB4DC #${buildNumber} \uC131\uACF5`));
|
|
1244
|
+
if (!versionCode) {
|
|
1245
|
+
versionCode = buildNumber;
|
|
1246
|
+
log(kleur.dim(` versionCode = buildNumber (${versionCode})`));
|
|
1247
|
+
}
|
|
1248
|
+
} else {
|
|
1249
|
+
if (!ciProvider) {
|
|
1250
|
+
log(kleur.red(`${kind} \uC124\uC815\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. setup-${kind} \uC2E4\uD589.`));
|
|
1251
|
+
process.exit(1);
|
|
1252
|
+
}
|
|
1253
|
+
const buildId = await runGitProviderBuild(ciProvider, args);
|
|
1254
|
+
if (!versionCode) {
|
|
1255
|
+
log(kleur.red(`\u2717 versionCode \uBBF8\uC9C0\uC815 (${kind} run_id ${buildId}\uB294 versionCode\uB85C \uBD80\uC801\uD569)`));
|
|
1256
|
+
log(kleur.dim(" \uAD8C\uC7A5 \uC0AC\uD56D:"));
|
|
1257
|
+
log(kleur.dim(" \u2022 CI \uC6CC\uD06C\uD50C\uB85C \uC548\uC5D0\uC11C versionCode\uB97C \uACB0\uC815\uD558\uACE0 \uACB0\uACFC\uB97C \uCD9C\uB825"));
|
|
1258
|
+
log(kleur.dim(" \u2022 \uB2E4\uC74C \uC2E4\uD589: mimi-seed deploy --skip-build --version-code <N>"));
|
|
1259
|
+
process.exit(1);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
if (!versionCode) {
|
|
1264
|
+
log(kleur.red("versionCode\uB97C \uC54C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. --version-code <N> \uC73C\uB85C \uC9C0\uC815\uD558\uC138\uC694."));
|
|
1265
|
+
process.exit(1);
|
|
1266
|
+
}
|
|
1267
|
+
let appId = args.appId;
|
|
1268
|
+
if (!appId) {
|
|
1269
|
+
const { mcpCall } = await import("./mcp-client-SLIKR42C.js");
|
|
1270
|
+
const r = await mcpCall(cfg.endpoint, cfg.token, "list_apps", {});
|
|
1271
|
+
if (!r.isError) {
|
|
1272
|
+
try {
|
|
1273
|
+
const apps = JSON.parse(r.text);
|
|
1274
|
+
if (apps.length > 0) {
|
|
1275
|
+
appId = apps[0].id;
|
|
1276
|
+
log(kleur.dim(` \uC571: ${apps[0].name} (${appId})`));
|
|
1277
|
+
}
|
|
1278
|
+
} catch {
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
if (!appId) {
|
|
1283
|
+
log(kleur.red("appId\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. --app <id> \uB85C \uC9C0\uC815\uD558\uAC70\uB098 `mimi-seed init` \uC73C\uB85C \uC571 \uB4F1\uB85D."));
|
|
1284
|
+
process.exit(1);
|
|
1285
|
+
}
|
|
1286
|
+
const needsConfirm = !args.dryRun && !args.yes && process.stdout.isTTY && !process.env.MIMI_SEED_TOKEN;
|
|
1287
|
+
if (needsConfirm) {
|
|
1288
|
+
const target = args.platform === "ios" ? "App Store \uC2EC\uC0AC \uC81C\uCD9C" : "Play Store production \uD2B8\uB799";
|
|
1289
|
+
log("");
|
|
1290
|
+
log(kleur.yellow(`\u26A0 \uC2E4\uC81C \uBC30\uD3EC: ${args.platform} \xB7 versionCode ${versionCode} \u2192 ${target}`));
|
|
1291
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1292
|
+
const answer = await new Promise(
|
|
1293
|
+
(resolve) => rl.question(kleur.bold("\uACC4\uC18D \uC9C4\uD589\uD560\uAE4C\uC694? [y/N]: "), (a) => resolve(a.trim().toLowerCase()))
|
|
1294
|
+
);
|
|
1295
|
+
rl.close();
|
|
1296
|
+
if (answer !== "y" && answer !== "yes") {
|
|
1297
|
+
log(kleur.dim("\uCDE8\uC18C\uB428. (--yes \uB85C \uD655\uC778 \uC0DD\uB7B5 \uAC00\uB2A5)"));
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
log("");
|
|
1302
|
+
log("\u{1F4E1} \uC11C\uBC84 \uBC30\uD3EC \uD30C\uC774\uD504\uB77C\uC778 \uC2DC\uC791...");
|
|
1303
|
+
log("");
|
|
1304
|
+
const deployBuildNumber = !args.skipBuild ? versionCode : void 0;
|
|
1305
|
+
await streamDeploy(cfg.webBase, cfg.token, {
|
|
1306
|
+
appId,
|
|
1307
|
+
platform: args.platform,
|
|
1308
|
+
versionCode,
|
|
1309
|
+
...deployBuildNumber ? { buildNumber: deployBuildNumber } : {},
|
|
1310
|
+
fromRef: args.fromRef,
|
|
1311
|
+
toRef: args.toRef,
|
|
1312
|
+
language: args.language,
|
|
1313
|
+
dryRun: args.dryRun
|
|
1314
|
+
});
|
|
1315
|
+
log("");
|
|
1316
|
+
log(kleur.bold("\uC644\uB8CC. Play Console\uC5D0\uC11C \uBC30\uD3EC \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uC138\uC694."));
|
|
1317
|
+
log(kleur.dim(" https://play.google.com/console/developers"));
|
|
1318
|
+
}
|
|
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
|
+
|
|
1524
|
+
// src/setup.ts
|
|
1525
|
+
function log2(msg = "") {
|
|
1526
|
+
process.stdout.write(msg + "\n");
|
|
1527
|
+
}
|
|
1528
|
+
var ALL_IDS = new Set(CREDENTIALS.map((c) => c.id));
|
|
1529
|
+
function parseIds(raw) {
|
|
1530
|
+
return raw.split(",").map((s) => s.trim()).filter((s) => ALL_IDS.has(s));
|
|
1531
|
+
}
|
|
1532
|
+
function parseSetupArgs(argv) {
|
|
1533
|
+
const opts = {
|
|
1534
|
+
yes: false,
|
|
1535
|
+
nonInteractive: false,
|
|
1536
|
+
interactive: false,
|
|
1537
|
+
failOnMissing: false
|
|
1538
|
+
};
|
|
1539
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1540
|
+
const a = argv[i];
|
|
1541
|
+
if (a === "--yes" || a === "-y") opts.yes = true;
|
|
1542
|
+
else if (a === "--non-interactive") opts.nonInteractive = true;
|
|
1543
|
+
else if (a === "--interactive") opts.interactive = true;
|
|
1544
|
+
else if (a === "--fail-on-missing") opts.failOnMissing = true;
|
|
1545
|
+
else if (a === "--only" && argv[i + 1]) opts.only = parseIds(argv[++i]);
|
|
1546
|
+
else if (a === "--reconnect" && argv[i + 1]) opts.reconnect = parseIds(argv[++i]);
|
|
1547
|
+
else if (a === "--platform" && argv[i + 1]) {
|
|
1548
|
+
opts.platforms = argv[++i].split(",").map((s) => s.trim()).filter((s) => s === "android" || s === "ios");
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
return opts;
|
|
1552
|
+
}
|
|
1553
|
+
function resolveMode(opts, env, stdinIsTTY) {
|
|
1554
|
+
if (opts.nonInteractive || opts.yes) return "report-only";
|
|
1555
|
+
if (opts.interactive) return "interactive";
|
|
1556
|
+
if (env.CI) return "report-only";
|
|
1557
|
+
if (!stdinIsTTY) return "report-only";
|
|
1558
|
+
return "interactive";
|
|
1559
|
+
}
|
|
1560
|
+
function statusLine(spec, detected, platforms) {
|
|
1561
|
+
const d = detected.get(spec.id);
|
|
1562
|
+
const relevant = spec.requirement !== "platform" || platforms.length === 0 || platforms.includes(spec.platform);
|
|
1563
|
+
let mark;
|
|
1564
|
+
if (d.present) mark = kleur2.green("\u2713");
|
|
1565
|
+
else if (isSatisfied(spec, detected)) mark = kleur2.yellow("~");
|
|
1566
|
+
else if (spec.requirement === "optional" || !relevant) mark = kleur2.dim("\xB7");
|
|
1567
|
+
else mark = kleur2.red("\u2717");
|
|
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}`;
|
|
1570
|
+
}
|
|
1571
|
+
function printStatus(detected, platforms) {
|
|
1572
|
+
const m = t().setup;
|
|
1573
|
+
log2(kleur2.bold(m.statusTitle + " ") + kleur2.dim(m.statusDir));
|
|
1574
|
+
log2();
|
|
1575
|
+
for (const group of ["core", "ci", "marketing"]) {
|
|
1576
|
+
const specs = CREDENTIALS.filter((c) => c.group === group);
|
|
1577
|
+
const title = group === "core" ? m.groupCore : group === "ci" ? m.groupCi : m.groupMarketing;
|
|
1578
|
+
log2(kleur2.dim(` \u2500\u2500 ${title} \u2500\u2500`));
|
|
1579
|
+
for (const spec of specs) log2(statusLine(spec, detected, platforms));
|
|
1580
|
+
log2();
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
function ask(q) {
|
|
1584
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
1585
|
+
return new Promise((resolve) => {
|
|
1586
|
+
let done = false;
|
|
1587
|
+
const finish = (value) => {
|
|
1588
|
+
if (done) return;
|
|
1589
|
+
done = true;
|
|
1590
|
+
rl.close();
|
|
1591
|
+
resolve(value);
|
|
1592
|
+
};
|
|
1593
|
+
rl.question(q, (a) => finish(a.trim()));
|
|
1594
|
+
rl.on("close", () => finish(""));
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
function printObtain(spec) {
|
|
1598
|
+
log2();
|
|
1599
|
+
log2(kleur2.bold(t().setup.obtainTitle(credLabel(spec))));
|
|
1600
|
+
for (const line of credObtain(spec)) log2(kleur2.dim(` ${line}`));
|
|
1601
|
+
if (spec.docsAnchor) {
|
|
1602
|
+
log2(kleur2.dim(t().setup.obtainMore(spec.docsAnchor)));
|
|
1603
|
+
}
|
|
1604
|
+
log2();
|
|
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
|
+
}
|
|
1615
|
+
async function connectOne(spec) {
|
|
1616
|
+
switch (spec.setup.kind) {
|
|
1617
|
+
case "mcp-bin": {
|
|
1618
|
+
const code = await runMcpBin(spec.setup.bin, spec.setup.args ?? []);
|
|
1619
|
+
if (code !== 0) {
|
|
1620
|
+
log2(kleur2.yellow(t().setup.binFailed(credLabel(spec), code, spec.fix)));
|
|
1621
|
+
}
|
|
1622
|
+
return true;
|
|
1623
|
+
}
|
|
1624
|
+
case "cli": {
|
|
1625
|
+
const provider = spec.setup.handler;
|
|
1626
|
+
const cfg = await promptGitProviderSetup(provider);
|
|
1627
|
+
log2(kleur2.dim(t().setup.verifying));
|
|
1628
|
+
const check = await verifyCiToken(cfg);
|
|
1629
|
+
if (!check.ok) {
|
|
1630
|
+
log2(kleur2.red(t().setup.verifyFailed(check.reason ?? "")));
|
|
1631
|
+
log2(kleur2.dim(t().setup.notSaved(spec.fix)));
|
|
1632
|
+
return true;
|
|
1633
|
+
}
|
|
1634
|
+
saveCiProviderConfig(cfg);
|
|
1635
|
+
log2(kleur2.green(t().setup.ciSaved(credLabel(spec), check.login ? ` (${check.login})` : "")));
|
|
1636
|
+
return true;
|
|
1637
|
+
}
|
|
1638
|
+
case "command": {
|
|
1639
|
+
log2(kleur2.yellow(t().setup.runSeparately(kleur2.cyan(spec.setup.run))));
|
|
1640
|
+
await ask(kleur2.dim(t().setup.pressEnter));
|
|
1641
|
+
return true;
|
|
1642
|
+
}
|
|
1643
|
+
case "env": {
|
|
1644
|
+
log2(kleur2.yellow(t().setup.envVar));
|
|
1645
|
+
log2(kleur2.cyan(` export ${spec.setup.envVar}=...`));
|
|
1646
|
+
log2(kleur2.dim(` (Windows PowerShell: $env:${spec.setup.envVar} = "..." )`));
|
|
1647
|
+
await ask(kleur2.dim(t().setup.pressEnter));
|
|
1648
|
+
return true;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
async function cmdSetup(argv) {
|
|
1653
|
+
const opts = parseSetupArgs(argv);
|
|
1654
|
+
const home = os6.homedir();
|
|
1655
|
+
migrateLegacyJenkins(home);
|
|
1656
|
+
if (resolveMode(opts, process.env, process.stdin.isTTY) === "interactive") {
|
|
1657
|
+
await ensureLangChosen();
|
|
1658
|
+
}
|
|
1659
|
+
const hints = await detectHints(process.cwd());
|
|
1660
|
+
const platforms = opts.platforms ?? [
|
|
1661
|
+
hints.some((h) => h.packageName) ? "android" : null,
|
|
1662
|
+
hints.some((h) => h.bundleId) ? "ios" : null
|
|
1663
|
+
].filter(Boolean);
|
|
1664
|
+
log2(kleur2.bold(t().setup.title));
|
|
1665
|
+
if (platforms.length > 0) log2(kleur2.dim(t().setup.platformsDetected(platforms.join(", "))));
|
|
1666
|
+
log2();
|
|
1667
|
+
let detected = detectAll(home);
|
|
1668
|
+
printStatus(detected, platforms);
|
|
1669
|
+
const mode = resolveMode(opts, process.env, process.stdin.isTTY);
|
|
1670
|
+
const plan = planSetup(detected, {
|
|
1671
|
+
only: opts.only,
|
|
1672
|
+
reconnect: opts.reconnect,
|
|
1673
|
+
platforms
|
|
1674
|
+
});
|
|
1675
|
+
if (mode === "report-only") {
|
|
1676
|
+
const missing = missingRequired(detected, platforms);
|
|
1677
|
+
if (missing.length > 0) {
|
|
1678
|
+
log2(kleur2.yellow(t().setup.missingRequired));
|
|
1679
|
+
for (const spec of missing) log2(kleur2.yellow(` \u2022 ${credLabel(spec)} \u2192 ${spec.fix}`));
|
|
1680
|
+
log2();
|
|
1681
|
+
}
|
|
1682
|
+
if (opts.only && plan.length > 0) {
|
|
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));
|
|
1686
|
+
process.exit(1);
|
|
1687
|
+
}
|
|
1688
|
+
log2(kleur2.dim(t().setup.runInTerminal));
|
|
1689
|
+
if (opts.failOnMissing && missing.length > 0) process.exit(1);
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
if (plan.length === 0) {
|
|
1693
|
+
if (opts.only) {
|
|
1694
|
+
log2(kleur2.green(t().setup.onlyAlreadyDone));
|
|
1695
|
+
log2(kleur2.dim(t().setup.onlyReconnectHint));
|
|
1696
|
+
} else {
|
|
1697
|
+
log2(kleur2.green(t().setup.allDone));
|
|
1698
|
+
}
|
|
1699
|
+
log2(kleur2.dim(" " + t().common.checkWith));
|
|
1700
|
+
if (opts.failOnMissing && missingRequired(detected, platforms).length > 0) process.exit(1);
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
log2(kleur2.dim(t().setup.planCount(plan.length)));
|
|
1704
|
+
log2();
|
|
1705
|
+
let aborted = false;
|
|
1706
|
+
for (const spec of plan) {
|
|
1707
|
+
if (aborted) break;
|
|
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})`);
|
|
1709
|
+
for (; ; ) {
|
|
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();
|
|
1714
|
+
if (answer === "?" || answer === "h") {
|
|
1715
|
+
printObtain(spec);
|
|
1716
|
+
continue;
|
|
1717
|
+
}
|
|
1718
|
+
if (answer === "q") {
|
|
1719
|
+
log2();
|
|
1720
|
+
log2(kleur2.dim(t().setup.quit));
|
|
1721
|
+
aborted = true;
|
|
1722
|
+
break;
|
|
1723
|
+
}
|
|
1724
|
+
if (answer === "s" || answer === "") {
|
|
1725
|
+
log2(kleur2.dim(t().setup.skipped(spec.fix)));
|
|
1726
|
+
log2();
|
|
1727
|
+
break;
|
|
1728
|
+
}
|
|
1729
|
+
if (answer === "c") {
|
|
1730
|
+
await connectOne(spec);
|
|
1731
|
+
log2();
|
|
1732
|
+
break;
|
|
1733
|
+
}
|
|
1734
|
+
log2(kleur2.dim(t().setup.promptInvalid));
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
detected = detectAll(home);
|
|
1738
|
+
log2();
|
|
1739
|
+
printStatus(detected, platforms);
|
|
1740
|
+
const stillMissing = missingRequired(detected, platforms);
|
|
1741
|
+
if (stillMissing.length === 0) {
|
|
1742
|
+
log2(kleur2.green(t().setup.requiredDone));
|
|
1743
|
+
} else {
|
|
1744
|
+
log2(kleur2.yellow(t().setup.stillMissing));
|
|
1745
|
+
for (const spec of stillMissing) log2(kleur2.yellow(` \u2022 ${credLabel(spec)} \u2192 ${spec.fix}`));
|
|
1746
|
+
}
|
|
1747
|
+
log2(kleur2.dim(t().setup.nextSteps));
|
|
1748
|
+
log2();
|
|
1749
|
+
if (opts.failOnMissing && stillMissing.length > 0) process.exit(1);
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
export {
|
|
1753
|
+
detectHints,
|
|
1754
|
+
hasAnyProjectSignal,
|
|
1755
|
+
writeConfig,
|
|
1756
|
+
deleteConfig,
|
|
1757
|
+
CONFIG_LOCATION,
|
|
1758
|
+
getEffectiveConfig,
|
|
1759
|
+
writeSettings,
|
|
1760
|
+
isLang,
|
|
1761
|
+
resolveLang,
|
|
1762
|
+
credLabel,
|
|
1763
|
+
credNote,
|
|
1764
|
+
CREDENTIALS,
|
|
1765
|
+
tryCredById,
|
|
1766
|
+
detectAll,
|
|
1767
|
+
isSatisfied,
|
|
1768
|
+
migrateLegacyJenkins,
|
|
1769
|
+
t,
|
|
1770
|
+
MCP_PKG,
|
|
1771
|
+
runMcpBin,
|
|
1772
|
+
cmdDeploy,
|
|
1773
|
+
parseSetupArgs,
|
|
1774
|
+
resolveMode,
|
|
1775
|
+
cmdSetup
|
|
1776
|
+
};
|