mimi-seed 0.20.0 → 0.21.1

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,143 +3,95 @@
3
3
  // src/setup.ts
4
4
  import kleur2 from "kleur";
5
5
  import * as readline2 from "readline";
6
- import os7 from "os";
6
+ import os8 from "os";
7
7
 
8
8
  // src/credentials.ts
9
9
  import fs3 from "fs";
10
10
  import os2 from "os";
11
- import path4 from "path";
11
+ import path3 from "path";
12
12
 
13
- // src/settings.ts
13
+ // ../core/src/lang.ts
14
14
  import fs from "fs";
15
15
  import os from "os";
16
- import path2 from "path";
17
-
18
- // src/lib/atomic-write.ts
19
- import { chmodSync, mkdirSync, renameSync, unlinkSync, writeFileSync } from "fs";
20
- import { randomUUID } from "crypto";
21
16
  import path from "path";
22
- var CREDENTIAL_FILE_MODE = 384;
23
- var CREDENTIAL_DIR_MODE = 448;
24
- var RENAME_RETRY_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES"]);
25
- var RENAME_RETRY_DELAYS_MS = [10, 20, 40, 80, 160, 320, 370];
26
- function sleepSync(ms) {
27
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
28
- }
29
- function renameWithRetry(from, to, rename = renameSync, sleep = sleepSync) {
30
- for (let attempt = 0; ; attempt += 1) {
31
- try {
32
- rename(from, to);
33
- return;
34
- } catch (error) {
35
- const code = error.code;
36
- if (!code || !RENAME_RETRY_CODES.has(code) || attempt >= RENAME_RETRY_DELAYS_MS.length) throw error;
37
- sleep(RENAME_RETRY_DELAYS_MS[attempt]);
38
- }
39
- }
40
- }
41
- function writeFileAtomic(filePath, contents, options = {}) {
42
- mkdirSync(path.dirname(filePath), { recursive: true, ...options.dirMode !== void 0 && { mode: options.dirMode } });
43
- const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
44
- try {
45
- writeFileSync(tempPath, contents, {
46
- encoding: "utf8",
47
- ...options.mode !== void 0 && { mode: options.mode }
48
- });
49
- if (options.mode !== void 0 && process.platform !== "win32") chmodSync(tempPath, options.mode);
50
- renameWithRetry(tempPath, filePath, options.rename, options.sleep);
51
- } catch (error) {
52
- try {
53
- unlinkSync(tempPath);
54
- } catch {
55
- }
56
- throw error;
57
- }
58
- }
59
- function writeJsonAtomic(filePath, value, options = {}) {
60
- writeFileAtomic(filePath, `${JSON.stringify(value, null, 2)}
61
- `, options);
62
- }
63
- function writeCredentialJson(filePath, value) {
64
- writeJsonAtomic(filePath, value, { mode: CREDENTIAL_FILE_MODE, dirMode: CREDENTIAL_DIR_MODE });
65
- }
66
-
67
- // src/settings.ts
68
17
  var DEFAULT_LANG = "ko";
69
- function settingsPath(home) {
70
- return path2.join(home, ".mimi-seed", "settings.json");
71
- }
72
- function readSettings(home = os.homedir()) {
73
- try {
74
- return JSON.parse(fs.readFileSync(settingsPath(home), "utf-8"));
75
- } catch {
76
- return {};
77
- }
78
- }
79
- function writeSettings(next, home = os.homedir()) {
80
- const merged = { ...readSettings(home), ...next };
81
- writeJsonAtomic(settingsPath(home), merged, { dirMode: 448 });
82
- }
83
- function isLangUnset(home = os.homedir()) {
84
- return !process.env.MIMI_SEED_LANG && !readSettings(home).lang;
85
- }
86
18
  function isLang(v) {
87
19
  return v === "ko" || v === "en";
88
20
  }
21
+ function settingsPath(home = os.homedir()) {
22
+ return path.join(home, ".mimi-seed", "settings.json");
23
+ }
89
24
  function resolveLang(home = os.homedir()) {
90
25
  const env = process.env.MIMI_SEED_LANG?.toLowerCase();
91
26
  if (isLang(env)) return env;
92
- const saved = readSettings(home).lang;
93
- if (isLang(saved)) return saved;
27
+ try {
28
+ const saved = JSON.parse(fs.readFileSync(settingsPath(home), "utf-8"))?.lang;
29
+ if (isLang(saved)) return saved;
30
+ } catch {
31
+ }
94
32
  return DEFAULT_LANG;
95
33
  }
96
34
 
97
- // src/project-manifest.ts
35
+ // ../core/src/project-manifest.ts
98
36
  import fs2 from "fs";
99
- import path3 from "path";
37
+ import path2 from "path";
100
38
  var MANIFEST_FILENAME = ".mimi-seed.json";
101
- function isValidSocialProfileId(value) {
102
- return /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(value);
103
- }
104
- function manifestSocialProfile(m, platform) {
105
- const profiles = m.socialProfiles;
106
- if (profiles === void 0) return null;
107
- if (!profiles || typeof profiles !== "object" || Array.isArray(profiles)) {
108
- throw new Error(`${MANIFEST_FILENAME} socialProfiles must be an object`);
109
- }
110
- const value = profiles[platform];
111
- if (value === void 0) return null;
112
- if (typeof value !== "string" || !isValidSocialProfileId(value)) {
113
- throw new Error(
114
- `${MANIFEST_FILENAME} socialProfiles.${platform} must be a safe 1-64 character profile id`
115
- );
116
- }
117
- return value;
118
- }
39
+ var SOCIAL_PROFILE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
119
40
  function findProjectManifest(startDir = process.cwd(), maxDepth = 8) {
120
- let dir = path3.resolve(startDir);
41
+ let dir = path2.resolve(startDir);
121
42
  for (let i = 0; i <= maxDepth; i++) {
122
- const candidate = path3.join(dir, MANIFEST_FILENAME);
43
+ const candidate = path2.join(dir, MANIFEST_FILENAME);
123
44
  if (fs2.existsSync(candidate)) {
124
- try {
125
- const obj = JSON.parse(fs2.readFileSync(candidate, "utf-8"));
126
- if (obj && typeof obj === "object") return { manifest: obj, filePath: candidate };
127
- } catch {
128
- }
45
+ const parsed = safeParse(candidate);
46
+ if (parsed) return { manifest: parsed, filePath: candidate };
129
47
  return null;
130
48
  }
131
- const parent = path3.dirname(dir);
49
+ const parent = path2.dirname(dir);
132
50
  if (parent === dir) break;
133
51
  dir = parent;
134
52
  }
135
53
  return null;
136
54
  }
55
+ function safeParse(filePath) {
56
+ try {
57
+ const raw = fs2.readFileSync(filePath, "utf-8");
58
+ const obj = JSON.parse(raw);
59
+ if (obj && typeof obj === "object") return obj;
60
+ return null;
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
137
65
  function manifestServiceEntries(m) {
138
66
  const svc = m.services ?? {};
139
67
  return Object.keys(svc).filter((k) => svc[k] != null).map((k) => [k, svc[k]]);
140
68
  }
69
+ function isValidSocialProfileId(value) {
70
+ return SOCIAL_PROFILE_ID_PATTERN.test(value);
71
+ }
72
+ var SOCIAL_PROFILE_MESSAGES_KO = {
73
+ notAnObject: `${MANIFEST_FILENAME}\uC758 socialProfiles\uB294 \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4.`,
74
+ invalidId: (platform) => `${MANIFEST_FILENAME}\uC758 socialProfiles.${platform} \uAC12\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uC601\uBB38\uC790\xB7\uC22B\uC790\uB85C \uC2DC\uC791\uD558\uB294 1~64\uC790\uC758 \uC601\uBB38\uC790/\uC22B\uC790/\uC810/\uBC11\uC904/\uD558\uC774\uD508\uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.`
75
+ };
76
+ function manifestSocialProfile(m, platform, messages = SOCIAL_PROFILE_MESSAGES_KO) {
77
+ const profiles = m.socialProfiles;
78
+ if (profiles === void 0) return null;
79
+ if (!profiles || typeof profiles !== "object" || Array.isArray(profiles)) {
80
+ throw new Error(messages.notAnObject);
81
+ }
82
+ const value = profiles[platform];
83
+ if (value === void 0) return null;
84
+ if (typeof value !== "string" || !isValidSocialProfileId(value)) {
85
+ throw new Error(messages.invalidId(platform));
86
+ }
87
+ return value;
88
+ }
141
89
 
142
90
  // src/credentials.ts
91
+ var SOCIAL_PROFILE_MESSAGES = {
92
+ notAnObject: `${MANIFEST_FILENAME} socialProfiles must be an object`,
93
+ invalidId: (platform) => `${MANIFEST_FILENAME} socialProfiles.${platform} must be a safe 1-64 character profile id`
94
+ };
143
95
  function credLabel(spec, lang = resolveLang()) {
144
96
  return spec.label[lang];
145
97
  }
@@ -150,10 +102,10 @@ function credObtain(spec, lang = resolveLang()) {
150
102
  return spec.obtain[lang];
151
103
  }
152
104
  function credDir(home) {
153
- return path4.join(home, ".mimi-seed");
105
+ return path3.join(home, ".mimi-seed");
154
106
  }
155
107
  function hasFile(home, name) {
156
- return fs3.existsSync(path4.join(credDir(home), name));
108
+ return fs3.existsSync(path3.join(credDir(home), name));
157
109
  }
158
110
  function anyFileStarting(home, prefix) {
159
111
  try {
@@ -164,7 +116,7 @@ function anyFileStarting(home, prefix) {
164
116
  }
165
117
  function readJson(home, name) {
166
118
  try {
167
- return JSON.parse(fs3.readFileSync(path4.join(credDir(home), name), "utf-8"));
119
+ return JSON.parse(fs3.readFileSync(path3.join(credDir(home), name), "utf-8"));
168
120
  } catch {
169
121
  return null;
170
122
  }
@@ -199,14 +151,14 @@ function detectProjectSocialToken(home, startDir, platform) {
199
151
  if (!loaded) return detectSocialToken(home, `${platform}.json`, idKey, tokenKey);
200
152
  let profile;
201
153
  try {
202
- profile = manifestSocialProfile(loaded.manifest, platform);
154
+ profile = manifestSocialProfile(loaded.manifest, platform, SOCIAL_PROFILE_MESSAGES);
203
155
  } catch (error) {
204
156
  return { present: false, detail: error instanceof Error ? error.message : String(error) };
205
157
  }
206
158
  if (!profile) return detectSocialToken(home, `${platform}.json`, idKey, tokenKey);
207
159
  const detected = detectSocialToken(
208
160
  home,
209
- path4.join("social-profiles", `${profile}.json`),
161
+ path3.join("social-profiles", `${profile}.json`),
210
162
  idKey,
211
163
  tokenKey,
212
164
  platform
@@ -219,7 +171,7 @@ function detectProjectSocialToken(home, startDir, platform) {
219
171
  function hasPlaySa(home) {
220
172
  if (hasFile(home, "play-service-account.json")) return true;
221
173
  try {
222
- return fs3.readdirSync(path4.join(credDir(home), "play-service-accounts")).some((f) => f.endsWith(".json"));
174
+ return fs3.readdirSync(path3.join(credDir(home), "play-service-accounts")).some((f) => f.endsWith(".json"));
223
175
  } catch {
224
176
  return false;
225
177
  }
@@ -716,7 +668,7 @@ function planSetup(detected, opts = {}) {
716
668
  // src/mcp-bin.ts
717
669
  import { spawn, spawnSync } from "child_process";
718
670
  import { existsSync, readFileSync } from "fs";
719
- import path5 from "path";
671
+ import path4 from "path";
720
672
  import { fileURLToPath } from "url";
721
673
 
722
674
  // src/i18n.ts
@@ -941,7 +893,7 @@ function t() {
941
893
  }
942
894
 
943
895
  // package.json
944
- var version = "0.20.0";
896
+ var version = "0.21.1";
945
897
 
946
898
  // src/mcp-bin.ts
947
899
  var MCP_PKG = "@yoonion/mimi-seed-mcp";
@@ -949,11 +901,11 @@ function resolveOnPath(bin, honorForceNpx = true) {
949
901
  if (honorForceNpx && process.env.MIMI_SEED_FORCE_NPX) return null;
950
902
  if (process.platform === "win32") {
951
903
  const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path");
952
- const directories = (pathKey ? process.env[pathKey] ?? "" : "").split(path5.delimiter).filter(Boolean);
904
+ const directories = (pathKey ? process.env[pathKey] ?? "" : "").split(path4.delimiter).filter(Boolean);
953
905
  const names = bin.toLowerCase().endsWith(".cmd") ? [bin] : [`${bin}.cmd`, bin];
954
906
  for (const directory of directories) {
955
907
  for (const name of names) {
956
- const candidate = path5.join(directory.replace(/^"|"$/g, ""), name);
908
+ const candidate = path4.join(directory.replace(/^"|"$/g, ""), name);
957
909
  if (existsSync(candidate)) return candidate;
958
910
  }
959
911
  }
@@ -969,13 +921,13 @@ function resolveWindowsShimTarget(shimPath, source) {
969
921
  const matches = [...source.matchAll(/["']([^"']+\.js)["']\s+%\*/gi)];
970
922
  const raw = matches.at(-1)?.[1];
971
923
  if (!raw) return null;
972
- return path5.win32.normalize(raw.replace(/%~?dp0%?/gi, `${path5.win32.dirname(shimPath)}${path5.win32.sep}`));
924
+ return path4.win32.normalize(raw.replace(/%~?dp0%?/gi, `${path4.win32.dirname(shimPath)}${path4.win32.sep}`));
973
925
  }
974
926
  function npxCliPath(shimPath) {
975
927
  const candidates = [
976
- shimPath ? path5.join(path5.dirname(shimPath), "node_modules", "npm", "bin", "npx-cli.js") : "",
977
- path5.join(path5.dirname(process.execPath), "node_modules", "npm", "bin", "npx-cli.js"),
978
- process.env.npm_execpath ? path5.join(path5.dirname(process.env.npm_execpath), "npx-cli.js") : ""
928
+ shimPath ? path4.join(path4.dirname(shimPath), "node_modules", "npm", "bin", "npx-cli.js") : "",
929
+ path4.join(path4.dirname(process.execPath), "node_modules", "npm", "bin", "npx-cli.js"),
930
+ process.env.npm_execpath ? path4.join(path4.dirname(process.env.npm_execpath), "npx-cli.js") : ""
979
931
  ];
980
932
  return candidates.find((candidate) => candidate && existsSync(candidate)) ?? null;
981
933
  }
@@ -1046,6 +998,59 @@ async function runMcpBin(bin, extraArgs = []) {
1046
998
  import fs4 from "fs";
1047
999
  import os3 from "os";
1048
1000
  import path6 from "path";
1001
+
1002
+ // ../core/src/atomic-write.ts
1003
+ import { chmodSync, mkdirSync, renameSync, unlinkSync, writeFileSync } from "fs";
1004
+ import { randomUUID } from "crypto";
1005
+ import path5 from "path";
1006
+ var CREDENTIAL_FILE_MODE = 384;
1007
+ var CREDENTIAL_DIR_MODE = 448;
1008
+ var RENAME_RETRY_DELAYS_MS = [10, 20, 40, 80, 160, 320, 370];
1009
+ var RENAME_RETRY_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES"]);
1010
+ function sleepSync(ms) {
1011
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1012
+ }
1013
+ function renameWithRetry(from, to, deps = {}) {
1014
+ const rename = deps.rename ?? renameSync;
1015
+ const sleep = deps.sleep ?? sleepSync;
1016
+ for (let attempt = 0; ; attempt += 1) {
1017
+ try {
1018
+ rename(from, to);
1019
+ return;
1020
+ } catch (error) {
1021
+ const code = error.code ?? "";
1022
+ if (!RENAME_RETRY_CODES.has(code) || attempt >= RENAME_RETRY_DELAYS_MS.length) throw error;
1023
+ sleep(RENAME_RETRY_DELAYS_MS[attempt]);
1024
+ }
1025
+ }
1026
+ }
1027
+ function writeFileAtomic(filePath, contents, options = {}) {
1028
+ mkdirSync(path5.dirname(filePath), { recursive: true, ...options.dirMode !== void 0 && { mode: options.dirMode } });
1029
+ const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
1030
+ try {
1031
+ writeFileSync(tempPath, contents, {
1032
+ encoding: "utf8",
1033
+ ...options.mode !== void 0 && { mode: options.mode }
1034
+ });
1035
+ if (options.mode !== void 0 && process.platform !== "win32") chmodSync(tempPath, options.mode);
1036
+ renameWithRetry(tempPath, filePath, options);
1037
+ } catch (error) {
1038
+ try {
1039
+ unlinkSync(tempPath);
1040
+ } catch {
1041
+ }
1042
+ throw error;
1043
+ }
1044
+ }
1045
+ function writeJsonAtomic(filePath, value, options = {}) {
1046
+ writeFileAtomic(filePath, `${JSON.stringify(value, null, 2)}
1047
+ `, options);
1048
+ }
1049
+ function writeCredentialJson(filePath, value) {
1050
+ writeJsonAtomic(filePath, value, { mode: CREDENTIAL_FILE_MODE, dirMode: CREDENTIAL_DIR_MODE });
1051
+ }
1052
+
1053
+ // src/jenkins-config.ts
1049
1054
  var CONFIG_DIR = path6.join(os3.homedir(), ".mimi-seed");
1050
1055
  var JENKINS_PATH = path6.join(CONFIG_DIR, "jenkins.json");
1051
1056
  var LEGACY_PATH = path6.join(CONFIG_DIR, "config.json");
@@ -1445,6 +1450,20 @@ async function consumeDeployStream(reader, onEvent) {
1445
1450
  import fs7 from "fs/promises";
1446
1451
  import path9 from "path";
1447
1452
 
1453
+ // ../core/src/http-errors.ts
1454
+ function endpointLabel(input, fallback) {
1455
+ try {
1456
+ const url = new URL(String(input));
1457
+ return `${url.host}${url.pathname}`;
1458
+ } catch {
1459
+ return fallback;
1460
+ }
1461
+ }
1462
+ function isTimeoutAbort(error) {
1463
+ const named = (e) => e?.name === "TimeoutError";
1464
+ return named(error) || named(error?.cause);
1465
+ }
1466
+
1448
1467
  // src/lib/http.ts
1449
1468
  var HTTP_TIMEOUT_MS = 3e4;
1450
1469
  var HTTP_STREAM_TIMEOUT_MS = 30 * 6e4;
@@ -1458,18 +1477,7 @@ var M2 = catalog(
1458
1477
  unknownEndpoint: "the remote server"
1459
1478
  }
1460
1479
  );
1461
- function endpointLabel(input) {
1462
- try {
1463
- const url = new URL(String(input));
1464
- return `${url.host}${url.pathname}`;
1465
- } catch {
1466
- return M2().unknownEndpoint;
1467
- }
1468
- }
1469
- function isTimeoutAbort(error) {
1470
- const named = (e) => e?.name === "TimeoutError";
1471
- return named(error) || named(error?.cause);
1472
- }
1480
+ var endpointLabel2 = (input) => endpointLabel(input, M2().unknownEndpoint);
1473
1481
  function isBodyAbort(error) {
1474
1482
  const named = (e) => {
1475
1483
  const name = e?.name;
@@ -1478,7 +1486,7 @@ function isBodyAbort(error) {
1478
1486
  return named(error) || named(error?.cause);
1479
1487
  }
1480
1488
  function timeoutError(input, timeoutMs, cause) {
1481
- return new Error(M2().timedOut(endpointLabel(input), Math.max(1, Math.round(timeoutMs / 1e3))), { cause });
1489
+ return new Error(M2().timedOut(endpointLabel2(input), Math.max(1, Math.round(timeoutMs / 1e3))), { cause });
1482
1490
  }
1483
1491
  async function readBodyWithTimeout(input, timeoutMs, read) {
1484
1492
  try {
@@ -1700,6 +1708,28 @@ function jenkinsBuildParameters(platform, ref, appId) {
1700
1708
  import fs9 from "fs";
1701
1709
  import path11 from "path";
1702
1710
  import os5 from "os";
1711
+
1712
+ // ../core/src/ci.ts
1713
+ function githubApiBase(cfg) {
1714
+ if (cfg.host) return `${cfg.host.replace(/\/$/, "")}/api/v3`;
1715
+ return "https://api.github.com";
1716
+ }
1717
+ function githubHeaders(token) {
1718
+ return {
1719
+ Authorization: `Bearer ${token}`,
1720
+ Accept: "application/vnd.github+json",
1721
+ "X-GitHub-Api-Version": "2022-11-28",
1722
+ "Content-Type": "application/json"
1723
+ };
1724
+ }
1725
+ function gitlabApiBase(cfg) {
1726
+ return `${cfg.host ?? "https://gitlab.com"}/api/v4`;
1727
+ }
1728
+ function gitlabProjectId(cfg) {
1729
+ return encodeURIComponent(`${cfg.owner}/${cfg.repo}`);
1730
+ }
1731
+
1732
+ // src/ci-providers.ts
1703
1733
  var CI_CONFIG_PATH = path11.join(os5.homedir(), ".mimi-seed", "ci.json");
1704
1734
  var M6 = catalog(
1705
1735
  {
@@ -1740,7 +1770,7 @@ async function verifyCiToken(cfg) {
1740
1770
  const probe = { ...cfg, host: normalizeHost(cfg.host) };
1741
1771
  try {
1742
1772
  if (probe.provider === "github") {
1743
- const res2 = await fetchWithTimeout(`${ghBase(probe)}/user`, {
1773
+ const res2 = await fetchWithTimeout(`${githubApiBase(probe)}/user`, {
1744
1774
  headers: { Authorization: `Bearer ${probe.token}`, Accept: "application/vnd.github+json" }
1745
1775
  });
1746
1776
  if (!res2.ok) {
@@ -1753,7 +1783,7 @@ async function verifyCiToken(cfg) {
1753
1783
  }
1754
1784
  return { ok: true, login: user2.login };
1755
1785
  }
1756
- const res = await fetchWithTimeout(`${glBase(probe)}/user`, { headers: { "PRIVATE-TOKEN": probe.token } });
1786
+ const res = await fetchWithTimeout(`${gitlabApiBase(probe)}/user`, { headers: { "PRIVATE-TOKEN": probe.token } });
1757
1787
  if (!res.ok) {
1758
1788
  return { ok: false, reason: M6().badToken("GitLab", res.status) };
1759
1789
  }
@@ -1763,26 +1793,14 @@ async function verifyCiToken(cfg) {
1763
1793
  return { ok: false, reason: e instanceof Error ? e.message : String(e) };
1764
1794
  }
1765
1795
  }
1766
- function ghBase(cfg) {
1767
- if (cfg.host) return `${cfg.host.replace(/\/$/, "")}/api/v3`;
1768
- return "https://api.github.com";
1769
- }
1770
- function ghHeaders(token) {
1771
- return {
1772
- Authorization: `Bearer ${token}`,
1773
- Accept: "application/vnd.github+json",
1774
- "X-GitHub-Api-Version": "2022-11-28",
1775
- "Content-Type": "application/json"
1776
- };
1777
- }
1778
1796
  async function ghTriggerWorkflow(cfg, workflow, ref, inputs = {}) {
1779
1797
  const startTime = /* @__PURE__ */ new Date();
1780
1798
  const wfId = /^\d+$/.test(workflow) ? Number(workflow) : workflow;
1781
1799
  const dispatchRes = await fetchWithTimeout(
1782
- `${ghBase(cfg)}/repos/${cfg.owner}/${cfg.repo}/actions/workflows/${wfId}/dispatches`,
1800
+ `${githubApiBase(cfg)}/repos/${cfg.owner}/${cfg.repo}/actions/workflows/${wfId}/dispatches`,
1783
1801
  {
1784
1802
  method: "POST",
1785
- headers: ghHeaders(cfg.token),
1803
+ headers: githubHeaders(cfg.token),
1786
1804
  body: JSON.stringify({ ref, inputs })
1787
1805
  }
1788
1806
  );
@@ -1791,8 +1809,8 @@ async function ghTriggerWorkflow(cfg, workflow, ref, inputs = {}) {
1791
1809
  }
1792
1810
  await new Promise((r) => setTimeout(r, 3e3));
1793
1811
  const runsRes = await fetchWithTimeout(
1794
- `${ghBase(cfg)}/repos/${cfg.owner}/${cfg.repo}/actions/workflows/${wfId}/runs?per_page=5`,
1795
- { headers: ghHeaders(cfg.token) }
1812
+ `${githubApiBase(cfg)}/repos/${cfg.owner}/${cfg.repo}/actions/workflows/${wfId}/runs?per_page=5`,
1813
+ { headers: githubHeaders(cfg.token) }
1796
1814
  );
1797
1815
  if (!runsRes.ok) return null;
1798
1816
  const data = await runsRes.json();
@@ -1808,8 +1826,8 @@ async function ghPollRun(cfg, runId, onTick, timeoutMs = 30 * 60 * 1e3, interval
1808
1826
  let res;
1809
1827
  try {
1810
1828
  res = await fetchWithTimeout(
1811
- `${ghBase(cfg)}/repos/${cfg.owner}/${cfg.repo}/actions/runs/${runId}`,
1812
- { headers: ghHeaders(cfg.token) }
1829
+ `${githubApiBase(cfg)}/repos/${cfg.owner}/${cfg.repo}/actions/runs/${runId}`,
1830
+ { headers: githubHeaders(cfg.token) }
1813
1831
  );
1814
1832
  } catch {
1815
1833
  consecutiveErrors++;
@@ -1832,17 +1850,11 @@ async function ghPollRun(cfg, runId, onTick, timeoutMs = 30 * 60 * 1e3, interval
1832
1850
  }
1833
1851
  return "timeout";
1834
1852
  }
1835
- function glBase(cfg) {
1836
- return `${cfg.host ?? "https://gitlab.com"}/api/v4`;
1837
- }
1838
- function glProjectId(cfg) {
1839
- return encodeURIComponent(`${cfg.owner}/${cfg.repo}`);
1840
- }
1841
1853
  async function glTriggerPipeline(cfg, ref, variables = {}) {
1842
1854
  const vars = Object.entries(variables).map(([key, value]) => ({ key, value }));
1843
1855
  const body = { ref };
1844
1856
  if (vars.length > 0) body.variables = vars;
1845
- const res = await fetchWithTimeout(`${glBase(cfg)}/projects/${glProjectId(cfg)}/pipeline`, {
1857
+ const res = await fetchWithTimeout(`${gitlabApiBase(cfg)}/projects/${gitlabProjectId(cfg)}/pipeline`, {
1846
1858
  method: "POST",
1847
1859
  headers: { "PRIVATE-TOKEN": cfg.token, "Content-Type": "application/json" },
1848
1860
  body: JSON.stringify(body)
@@ -1859,7 +1871,7 @@ async function glPollPipeline(cfg, pipelineId, onTick, timeoutMs = 30 * 60 * 1e3
1859
1871
  let res;
1860
1872
  try {
1861
1873
  res = await fetchWithTimeout(
1862
- `${glBase(cfg)}/projects/${glProjectId(cfg)}/pipelines/${pipelineId}`,
1874
+ `${gitlabApiBase(cfg)}/projects/${gitlabProjectId(cfg)}/pipelines/${pipelineId}`,
1863
1875
  { headers: { "PRIVATE-TOKEN": cfg.token } }
1864
1876
  );
1865
1877
  } catch {
@@ -2532,9 +2544,27 @@ async function cmdDeploy(argv) {
2532
2544
  log(kleur.bold(M7().done));
2533
2545
  }
2534
2546
 
2535
- // src/telemetry.ts
2547
+ // src/settings.ts
2536
2548
  import fs10 from "fs";
2537
2549
  import os6 from "os";
2550
+ function readSettings(home = os6.homedir()) {
2551
+ try {
2552
+ return JSON.parse(fs10.readFileSync(settingsPath(home), "utf-8"));
2553
+ } catch {
2554
+ return {};
2555
+ }
2556
+ }
2557
+ function writeSettings(next, home = os6.homedir()) {
2558
+ const merged = { ...readSettings(home), ...next };
2559
+ writeJsonAtomic(settingsPath(home), merged, { dirMode: 448 });
2560
+ }
2561
+ function isLangUnset(home = os6.homedir()) {
2562
+ return !process.env.MIMI_SEED_LANG && !readSettings(home).lang;
2563
+ }
2564
+
2565
+ // src/telemetry.ts
2566
+ import fs11 from "fs";
2567
+ import os7 from "os";
2538
2568
  import path12 from "path";
2539
2569
  import { createHmac, randomUUID as randomUUID3 } from "crypto";
2540
2570
  var ENDPOINT = "https://mimi-seed.pryzm.gg/api/sdk-usage";
@@ -2552,10 +2582,10 @@ var M8 = catalog(
2552
2582
  invalid: "Usage: mimi-seed telemetry on|off|status\n"
2553
2583
  }
2554
2584
  );
2555
- var location = () => path12.join(os6.homedir(), ".mimi-seed", "telemetry.json");
2585
+ var location = () => path12.join(os7.homedir(), ".mimi-seed", "telemetry.json");
2556
2586
  function readConsent() {
2557
2587
  try {
2558
- return JSON.parse(fs10.readFileSync(location(), "utf8"));
2588
+ return JSON.parse(fs11.readFileSync(location(), "utf8"));
2559
2589
  } catch {
2560
2590
  return { enabled: false };
2561
2591
  }
@@ -2585,9 +2615,9 @@ function cmdTelemetry(argv) {
2585
2615
  process.stdout.write(telemetryEnabled() ? M8().enabled : M8().disabled);
2586
2616
  }
2587
2617
  function frameworkAt(root) {
2588
- if (fs10.existsSync(path12.join(root, "ProjectSettings", "ProjectSettings.asset"))) return "unity";
2618
+ if (fs11.existsSync(path12.join(root, "ProjectSettings", "ProjectSettings.asset"))) return "unity";
2589
2619
  try {
2590
- const pkg = JSON.parse(fs10.readFileSync(path12.join(root, "package.json"), "utf8"));
2620
+ const pkg = JSON.parse(fs11.readFileSync(path12.join(root, "package.json"), "utf8"));
2591
2621
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
2592
2622
  if (deps.expo) return "expo";
2593
2623
  if (deps["react-native"]) return "react-native";
@@ -2605,7 +2635,7 @@ function usageRun(command, projectPath) {
2605
2635
  if (!consent.installationId || !consent.salt) writeConsent({ enabled: consent.enabled, installationId, salt });
2606
2636
  let normalized = path12.resolve(projectPath);
2607
2637
  try {
2608
- normalized = fs10.realpathSync(normalized);
2638
+ normalized = fs11.realpathSync(normalized);
2609
2639
  } catch {
2610
2640
  }
2611
2641
  if (process.platform === "win32") normalized = normalized.toLowerCase();
@@ -2778,7 +2808,7 @@ async function connectOne(spec) {
2778
2808
  }
2779
2809
  async function cmdSetup(argv) {
2780
2810
  const opts = parseSetupArgs(argv);
2781
- const home = os7.homedir();
2811
+ const home = os8.homedir();
2782
2812
  migrateLegacyJenkins(home);
2783
2813
  if (resolveMode(opts, process.env, process.stdin.isTTY) === "interactive") {
2784
2814
  await ensureLangChosen();
@@ -2889,7 +2919,6 @@ export {
2889
2919
  deleteConfig,
2890
2920
  CONFIG_LOCATION,
2891
2921
  getEffectiveConfig,
2892
- writeSettings,
2893
2922
  isLang,
2894
2923
  resolveLang,
2895
2924
  catalog,
@@ -2913,6 +2942,7 @@ export {
2913
2942
  findProjectLink,
2914
2943
  linkProject,
2915
2944
  cmdDeploy,
2945
+ writeSettings,
2916
2946
  telemetryNotice,
2917
2947
  cmdTelemetry,
2918
2948
  usageRun,
package/dist/index.js CHANGED
@@ -32,7 +32,7 @@ import {
32
32
  version,
33
33
  writeConfig,
34
34
  writeSettings
35
- } from "./chunk-R2ZG7DVZ.js";
35
+ } from "./chunk-GWBZKH65.js";
36
36
 
37
37
  // src/index.ts
38
38
  import kleur11 from "kleur";
@@ -283,7 +283,7 @@ async function cmdAuth(args) {
283
283
  if (sub === "threads") return void exitWith(await runMcpBin("mimi-seed-social-auth", ["threads", ...rest]));
284
284
  if (sub === "tiktok") return void exitWith(await runMcpBin("mimi-seed-tiktok-business-auth", rest));
285
285
  if (sub === "ci") {
286
- const { cmdSetup: cmdSetup2 } = await import("./setup-6FGYI72M.js");
286
+ const { cmdSetup: cmdSetup2 } = await import("./setup-5XA5MVLT.js");
287
287
  await cmdSetup2(["--only", "github,gitlab", "--reconnect", "github,gitlab"]);
288
288
  return;
289
289
  }
@@ -927,7 +927,7 @@ async function cmdDoctor(args = []) {
927
927
  // src/check.ts
928
928
  import kleur5 from "kleur";
929
929
 
930
- // src/checks/release-doctor-render.ts
930
+ // ../core/src/checks/release-doctor-render.ts
931
931
  var COPY = {
932
932
  ko: {
933
933
  title: "Mimi Seed Release Doctor \u2014 \uB85C\uADF8\uC778 \uC5C6\uB294 \uCD9C\uC2DC \uC810\uAC80",
@@ -990,11 +990,11 @@ function renderReleaseDoctor(report, lang) {
990
990
  return lines.join("\n");
991
991
  }
992
992
 
993
- // src/checks/release-doctor.ts
993
+ // ../core/src/checks/release-doctor.ts
994
994
  import fs5 from "fs/promises";
995
995
  import path5 from "path";
996
996
 
997
- // src/checks/billing.ts
997
+ // ../core/src/checks/billing.ts
998
998
  import fs4 from "fs/promises";
999
999
  import path4 from "path";
1000
1000
  var BILLING_MODULE = /com\.android\.billingclient:billing(?:-ktx)?/;
@@ -1400,7 +1400,7 @@ async function checkBillingCompliance(projectPath, now = /* @__PURE__ */ new Dat
1400
1400
  };
1401
1401
  }
1402
1402
 
1403
- // src/checks/release-doctor.ts
1403
+ // ../core/src/checks/release-doctor.ts
1404
1404
  var SKIP_DIRS2 = /* @__PURE__ */ new Set([
1405
1405
  ".git",
1406
1406
  ".gradle",
@@ -2753,8 +2753,24 @@ import { createInterface } from "readline/promises";
2753
2753
  import Anthropic from "@anthropic-ai/sdk";
2754
2754
  import kleur6 from "kleur";
2755
2755
 
2756
- // src/ai-model.ts
2757
- var CLI_AI_MODEL = "claude-haiku-4-5-20251001";
2756
+ // ../core/src/ai.ts
2757
+ var AI_MODEL = "claude-haiku-4-5-20251001";
2758
+ var SENTIMENT_KEYWORDS = [
2759
+ ["bug_report", ["\uBC84\uADF8", "\uC624\uB958", "\uC548\uB428", "crash", "bug", "error", "broken"]],
2760
+ ["feature_request", ["\uCD94\uAC00", "\uC6D0\uD574", "\uC788\uC73C\uBA74", "wish", "feature", "add", "would like"]],
2761
+ ["negative", ["\uBCC4\uB85C", "\uC2E4\uB9DD", "\uC9DC\uC99D", "terrible", "worst", "awful"]],
2762
+ ["positive", ["\uC88B\uC544", "\uCD5C\uACE0", "\uD6CC\uB96D", "great", "excellent", "love", "perfect"]]
2763
+ ];
2764
+ function detectReviewSentiment(text) {
2765
+ const lower = text.toLowerCase();
2766
+ for (const [sentiment, keywords] of SENTIMENT_KEYWORDS) {
2767
+ if (keywords.some((w) => lower.includes(w))) return sentiment;
2768
+ }
2769
+ return "neutral";
2770
+ }
2771
+ var REVIEW_REPLY_MAX_TOKENS = 500;
2772
+ var RELEASE_NOTES_MAX_TOKENS = 2e3;
2773
+ var RELEASE_NOTE_TONES = ["concise", "detailed", "marketing"];
2758
2774
 
2759
2775
  // src/notes.ts
2760
2776
  var M7 = catalog(
@@ -2762,19 +2778,17 @@ var M7 = catalog(
2762
2778
  // Claude 프롬프트
2763
2779
  localeHint: (l) => `"${l}": "\uD574\uB2F9 \uC5B8\uC5B4\uB85C \uBC88\uC5ED\uB41C \uAC04\uACB0\uD55C \uBC84\uC804"`,
2764
2780
  system: "\uC571 \uC2A4\uD1A0\uC5B4 \uB9B4\uB9AC\uC988 \uB178\uD2B8 \uC804\uBB38 \uCE74\uD53C\uB77C\uC774\uD130\uC785\uB2C8\uB2E4. \uCEE4\uBC0B \uB0B4\uC5ED\uC744 \uC0AC\uC6A9\uC790 \uCE5C\uD654\uC801\uC778 \uC5B8\uC5B4\uB85C \uBCC0\uD658\uD569\uB2C8\uB2E4. \uD56D\uC0C1 \uC720\uD6A8\uD55C JSON\uC73C\uB85C\uB9CC \uC751\uB2F5\uD558\uC138\uC694.",
2765
- userPrompt: (commitsText, localeList) => `\uB2E4\uC74C \uCEE4\uBC0B \uB0B4\uC5ED\uC73C\uB85C \uB9B4\uB9AC\uC988 \uB178\uD2B8\uB97C 3\uAC00\uC9C0 \uD1A4\uC73C\uB85C \uC791\uC131\uD558\uC138\uC694:
2781
+ toneHints: {
2782
+ concise: "\uAC04\uACB0\uD55C \uBC84\uC804 (3\uC904 \uC774\uB0B4, \uBD88\uB9BF)",
2783
+ detailed: "\uC0C1\uC138 \uBC84\uC804 (5\uAC1C \uC774\uB0B4, \uBD88\uB9BF)",
2784
+ marketing: "\uB9C8\uCF00\uD305 \uBC84\uC804 (\uC5F4\uC815\uC801 \uD1A4)"
2785
+ },
2786
+ userPrompt: (toneCount, commitsText, skeleton) => `\uB2E4\uC74C \uCEE4\uBC0B \uB0B4\uC5ED\uC73C\uB85C \uB9B4\uB9AC\uC988 \uB178\uD2B8\uB97C ${toneCount}\uAC00\uC9C0 \uD1A4\uC73C\uB85C \uC791\uC131\uD558\uC138\uC694:
2766
2787
 
2767
2788
  ${commitsText}
2768
2789
 
2769
2790
  JSON:
2770
- {
2771
- "concise": "\uAC04\uACB0\uD55C \uBC84\uC804 (3\uC904 \uC774\uB0B4, \uBD88\uB9BF)",
2772
- "detailed": "\uC0C1\uC138 \uBC84\uC804 (5\uAC1C \uC774\uB0B4, \uBD88\uB9BF)",
2773
- "marketing": "\uB9C8\uCF00\uD305 \uBC84\uC804 (\uC5F4\uC815\uC801 \uD1A4)",
2774
- "localized": {
2775
- ${localeList}
2776
- }
2777
- }`,
2791
+ ${skeleton}`,
2778
2792
  parseFailed: "AI \uC751\uB2F5 \uD30C\uC2F1 \uC2E4\uD328",
2779
2793
  // 템플릿 폴백
2780
2794
  marketingTemplate: (items) => `\uC0C8\uB85C\uC6B4 \uC5C5\uB370\uC774\uD2B8\uAC00 \uC900\uBE44\uB410\uC2B5\uB2C8\uB2E4!
@@ -2819,19 +2833,17 @@ ${items}
2819
2833
  // Claude prompt
2820
2834
  localeHint: (l) => `"${l}": "the concise version, translated into that language"`,
2821
2835
  system: "You are an expert app store release-notes copywriter. You turn commit history into user-friendly language. Always respond with valid JSON only.",
2822
- userPrompt: (commitsText, localeList) => `Write release notes in 3 tones from the following commit history:
2836
+ toneHints: {
2837
+ concise: "concise version (3 bullets max)",
2838
+ detailed: "detailed version (5 bullets max)",
2839
+ marketing: "marketing version (enthusiastic tone)"
2840
+ },
2841
+ userPrompt: (toneCount, commitsText, skeleton) => `Write release notes in ${toneCount} tones from the following commit history:
2823
2842
 
2824
2843
  ${commitsText}
2825
2844
 
2826
2845
  JSON:
2827
- {
2828
- "concise": "concise version (3 bullets max)",
2829
- "detailed": "detailed version (5 bullets max)",
2830
- "marketing": "marketing version (enthusiastic tone)",
2831
- "localized": {
2832
- ${localeList}
2833
- }
2834
- }`,
2846
+ ${skeleton}`,
2835
2847
  parseFailed: "Failed to parse the AI response",
2836
2848
  // Template fallback
2837
2849
  marketingTemplate: (items) => `A new update is here!
@@ -2891,18 +2903,30 @@ async function promptUser(question) {
2891
2903
  rl.close();
2892
2904
  return answer.trim();
2893
2905
  }
2906
+ function buildReleaseNotesPrompt(commitsText, locales) {
2907
+ const m = M7();
2908
+ const toneHints = m.toneHints;
2909
+ const localeList = locales.map((l) => m.localeHint(l)).join(",\n ");
2910
+ const skeleton = [
2911
+ "{",
2912
+ ...RELEASE_NOTE_TONES.map((tone) => ` "${tone}": "${toneHints[tone]}",`),
2913
+ ` "localized": {
2914
+ ${localeList}
2915
+ }`,
2916
+ "}"
2917
+ ].join("\n");
2918
+ return m.userPrompt(RELEASE_NOTE_TONES.length, commitsText, skeleton);
2919
+ }
2894
2920
  async function generateWithClaude(commitsText, locales) {
2895
2921
  const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
2896
- const localeList = locales.map((l) => M7().localeHint(l)).join(",\n ");
2897
2922
  const response = await client.messages.create({
2898
- model: CLI_AI_MODEL,
2899
- // mcp-server 의 generateReleaseNotes 기본값과 같아야 한다 — 낮으면 다국어 JSON 이
2900
- // CLI 경로에서만 잘린다. ai-parity.test.ts 가 두 값의 일치를 강제한다.
2901
- max_tokens: 2e3,
2923
+ model: AI_MODEL,
2924
+ // mcp-server 의 generateReleaseNotes 기본값과 같은 상수 — 낮으면 다국어 JSON 이 한쪽 경로에서만 잘린다.
2925
+ max_tokens: RELEASE_NOTES_MAX_TOKENS,
2902
2926
  system: M7().system,
2903
2927
  messages: [{
2904
2928
  role: "user",
2905
- content: M7().userPrompt(commitsText, localeList)
2929
+ content: buildReleaseNotesPrompt(commitsText, locales)
2906
2930
  }]
2907
2931
  });
2908
2932
  const text = response.content[0].type === "text" ? response.content[0].text : "";
@@ -3161,28 +3185,20 @@ async function promptUser2(question) {
3161
3185
  rl.close();
3162
3186
  return answer.trim();
3163
3187
  }
3164
- function detectSentiment(text) {
3165
- const lower = text.toLowerCase();
3166
- if (["\uBC84\uADF8", "\uC624\uB958", "\uC548\uB428", "crash", "bug", "error", "broken"].some((w) => lower.includes(w))) return "bug_report";
3167
- if (["\uCD94\uAC00", "\uC6D0\uD574", "\uC788\uC73C\uBA74", "wish", "feature", "add", "would like"].some((w) => lower.includes(w))) return "feature_request";
3168
- if (["\uBCC4\uB85C", "\uC2E4\uB9DD", "\uC9DC\uC99D", "terrible", "worst", "awful"].some((w) => lower.includes(w))) return "negative";
3169
- if (["\uC88B\uC544", "\uCD5C\uACE0", "\uD6CC\uB96D", "great", "excellent", "love", "perfect"].some((w) => lower.includes(w))) return "positive";
3170
- return "neutral";
3171
- }
3172
3188
  async function generateReply(opts) {
3173
3189
  const client = new Anthropic2({ apiKey: process.env.ANTHROPIC_API_KEY });
3174
3190
  const m = M8();
3175
3191
  const toneGuides = m.toneGuides;
3176
3192
  const sentimentPrompts = m.sentimentPrompts;
3177
3193
  const localeNames = m.localeNames;
3178
- const sentiment = detectSentiment(opts.text);
3194
+ const sentiment = detectReviewSentiment(opts.text);
3179
3195
  const toneGuide = toneGuides[opts.tone] ?? toneGuides.friendly;
3180
3196
  const sentimentPrompt = sentimentPrompts[sentiment] ?? sentimentPrompts.neutral;
3181
3197
  const stars = opts.rating !== void 0 ? m.starsLabel(`${"\u2605".repeat(opts.rating)}${"\u2606".repeat(5 - opts.rating)}`, opts.rating) : "";
3182
3198
  const langName = localeNames[opts.language] ?? opts.language;
3183
3199
  const response = await client.messages.create({
3184
- model: CLI_AI_MODEL,
3185
- max_tokens: 500,
3200
+ model: AI_MODEL,
3201
+ max_tokens: REVIEW_REPLY_MAX_TOKENS,
3186
3202
  system: m.system(langName, toneGuide, opts.developerName ?? m.defaultDeveloper),
3187
3203
  messages: [{
3188
3204
  role: "user",
@@ -3,7 +3,7 @@ import {
3
3
  cmdSetup,
4
4
  parseSetupArgs,
5
5
  resolveMode
6
- } from "./chunk-R2ZG7DVZ.js";
6
+ } from "./chunk-GWBZKH65.js";
7
7
  export {
8
8
  cmdSetup,
9
9
  parseSetupArgs,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mimi-seed",
3
3
  "keywords": ["release-doctor", "expo", "react-native", "unity", "google-play", "app-store", "release-readiness", "mcp", "cli"],
4
- "version": "0.20.0",
4
+ "version": "0.21.1",
5
5
  "description": "Release Doctor: check Expo, React Native and Unity release risks before connecting app stores.",
6
6
  "bin": {
7
7
  "mimi-seed": "dist/index.js"
@@ -19,11 +19,9 @@
19
19
  "LICENSE"
20
20
  ],
21
21
  "scripts": {
22
- "prebuild": "node ../../scripts/sync-release-doctor.mjs --check",
23
22
  "build": "tsup",
24
23
  "dev": "tsx src/index.ts",
25
24
  "typecheck": "tsc --noEmit",
26
- "pretest": "node ../../scripts/sync-release-doctor.mjs --check",
27
25
  "test": "npm run typecheck && npm run lint && vitest run",
28
26
  "test:watch": "vitest",
29
27
  "lint": "eslint .",