mimi-seed 0.2.6 → 0.2.8

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.
Files changed (2) hide show
  1. package/dist/index.js +332 -68
  2. package/package.json +6 -3
package/dist/index.js CHANGED
@@ -4,9 +4,9 @@ import {
4
4
  } from "./chunk-Q5YGYAFK.js";
5
5
 
6
6
  // src/index.ts
7
- import os3 from "os";
8
- import fs4 from "fs";
9
- import path4 from "path";
7
+ import os4 from "os";
8
+ import fs5 from "fs";
9
+ import path5 from "path";
10
10
  import kleur8 from "kleur";
11
11
  import open from "open";
12
12
 
@@ -853,6 +853,149 @@ async function cmdAuth(args) {
853
853
  // src/deploy.ts
854
854
  import kleur6 from "kleur";
855
855
  import * as readline from "readline";
856
+
857
+ // src/ci-providers.ts
858
+ import fs3 from "fs";
859
+ import path3 from "path";
860
+ import os2 from "os";
861
+ var CI_CONFIG_PATH = path3.join(os2.homedir(), ".mimi-seed", "ci.json");
862
+ function loadCiProviderConfig() {
863
+ try {
864
+ return JSON.parse(fs3.readFileSync(CI_CONFIG_PATH, "utf-8"));
865
+ } catch {
866
+ return null;
867
+ }
868
+ }
869
+ function saveCiProviderConfig(cfg) {
870
+ const dir = path3.dirname(CI_CONFIG_PATH);
871
+ if (!fs3.existsSync(dir)) {
872
+ fs3.mkdirSync(dir, { recursive: true, mode: 448 });
873
+ }
874
+ fs3.writeFileSync(CI_CONFIG_PATH, JSON.stringify(cfg, null, 2));
875
+ if (process.platform !== "win32") {
876
+ fs3.chmodSync(CI_CONFIG_PATH, 384);
877
+ }
878
+ }
879
+ function ghBase(cfg) {
880
+ if (cfg.host) return `${cfg.host.replace(/\/$/, "")}/api/v3`;
881
+ return "https://api.github.com";
882
+ }
883
+ function ghHeaders(token) {
884
+ return {
885
+ Authorization: `Bearer ${token}`,
886
+ Accept: "application/vnd.github+json",
887
+ "X-GitHub-Api-Version": "2022-11-28",
888
+ "Content-Type": "application/json"
889
+ };
890
+ }
891
+ async function ghTriggerWorkflow(cfg, workflow, ref, inputs = {}) {
892
+ const startTime = /* @__PURE__ */ new Date();
893
+ const wfId = /^\d+$/.test(workflow) ? Number(workflow) : workflow;
894
+ const dispatchRes = await fetch(
895
+ `${ghBase(cfg)}/repos/${cfg.owner}/${cfg.repo}/actions/workflows/${wfId}/dispatches`,
896
+ {
897
+ method: "POST",
898
+ headers: ghHeaders(cfg.token),
899
+ body: JSON.stringify({ ref, inputs })
900
+ }
901
+ );
902
+ if (!dispatchRes.ok) {
903
+ throw new Error(`GitHub dispatch ${dispatchRes.status}: ${await dispatchRes.text()}`);
904
+ }
905
+ await new Promise((r) => setTimeout(r, 3e3));
906
+ const runsRes = await fetch(
907
+ `${ghBase(cfg)}/repos/${cfg.owner}/${cfg.repo}/actions/workflows/${wfId}/runs?per_page=5`,
908
+ { headers: ghHeaders(cfg.token) }
909
+ );
910
+ if (!runsRes.ok) return null;
911
+ const data = await runsRes.json();
912
+ const run = data.workflow_runs.find((r) => new Date(r.created_at) >= startTime) ?? data.workflow_runs[0];
913
+ if (!run) return null;
914
+ return { runId: run.id, url: run.html_url };
915
+ }
916
+ async function ghPollRun(cfg, runId, onTick, timeoutMs = 30 * 60 * 1e3, intervalMs = 15e3) {
917
+ const start = Date.now();
918
+ let consecutiveErrors = 0;
919
+ while (Date.now() - start < timeoutMs) {
920
+ await new Promise((r) => setTimeout(r, intervalMs));
921
+ let res;
922
+ try {
923
+ res = await fetch(
924
+ `${ghBase(cfg)}/repos/${cfg.owner}/${cfg.repo}/actions/runs/${runId}`,
925
+ { headers: ghHeaders(cfg.token) }
926
+ );
927
+ } catch {
928
+ consecutiveErrors++;
929
+ if (consecutiveErrors >= 3) throw new Error("GitHub API \uC5F0\uC18D \uC624\uB958 3\uD68C");
930
+ continue;
931
+ }
932
+ if (!res.ok) {
933
+ consecutiveErrors++;
934
+ if (consecutiveErrors >= 3) throw new Error(`GitHub API \uC5F0\uC18D \uC624\uB958 (HTTP ${res.status})`);
935
+ continue;
936
+ }
937
+ consecutiveErrors = 0;
938
+ const data = await res.json();
939
+ onTick?.(data.status);
940
+ if (data.status === "completed") {
941
+ if (data.conclusion === "success") return "success";
942
+ if (data.conclusion === "cancelled") return "cancelled";
943
+ return "failure";
944
+ }
945
+ }
946
+ return "timeout";
947
+ }
948
+ function glBase(cfg) {
949
+ return `${cfg.host ?? "https://gitlab.com"}/api/v4`;
950
+ }
951
+ function glProjectId(cfg) {
952
+ return encodeURIComponent(`${cfg.owner}/${cfg.repo}`);
953
+ }
954
+ async function glTriggerPipeline(cfg, ref, variables = {}) {
955
+ const vars = Object.entries(variables).map(([key, value]) => ({ key, value }));
956
+ const body = { ref };
957
+ if (vars.length > 0) body.variables = vars;
958
+ const res = await fetch(`${glBase(cfg)}/projects/${glProjectId(cfg)}/pipeline`, {
959
+ method: "POST",
960
+ headers: { "PRIVATE-TOKEN": cfg.token, "Content-Type": "application/json" },
961
+ body: JSON.stringify(body)
962
+ });
963
+ if (!res.ok) throw new Error(`GitLab trigger ${res.status}: ${await res.text()}`);
964
+ const data = await res.json();
965
+ return { pipelineId: data.id, url: data.web_url };
966
+ }
967
+ async function glPollPipeline(cfg, pipelineId, onTick, timeoutMs = 30 * 60 * 1e3, intervalMs = 15e3) {
968
+ const start = Date.now();
969
+ let consecutiveErrors = 0;
970
+ while (Date.now() - start < timeoutMs) {
971
+ await new Promise((r) => setTimeout(r, intervalMs));
972
+ let res;
973
+ try {
974
+ res = await fetch(
975
+ `${glBase(cfg)}/projects/${glProjectId(cfg)}/pipelines/${pipelineId}`,
976
+ { headers: { "PRIVATE-TOKEN": cfg.token } }
977
+ );
978
+ } catch {
979
+ consecutiveErrors++;
980
+ if (consecutiveErrors >= 3) throw new Error("GitLab API \uC5F0\uC18D \uC624\uB958 3\uD68C");
981
+ continue;
982
+ }
983
+ if (!res.ok) {
984
+ consecutiveErrors++;
985
+ if (consecutiveErrors >= 3) throw new Error(`GitLab API \uC5F0\uC18D \uC624\uB958 (HTTP ${res.status})`);
986
+ continue;
987
+ }
988
+ consecutiveErrors = 0;
989
+ const data = await res.json();
990
+ onTick?.(data.status);
991
+ if (data.status === "success") return "success";
992
+ if (data.status === "failed") return "failure";
993
+ if (data.status === "canceled" || data.status === "skipped") return "cancelled";
994
+ }
995
+ return "timeout";
996
+ }
997
+
998
+ // src/deploy.ts
856
999
  var PHASE_ICON = {
857
1000
  init: "\u{1F680}",
858
1001
  verify: "\u{1F50D}",
@@ -963,7 +1106,17 @@ async function streamDeploy(webBase, token, body) {
963
1106
  }
964
1107
  }
965
1108
  function parseArgs4(argv) {
966
- const args = { platform: "android", language: "ko-KR", dryRun: false, skipBuild: false, setupJenkins: false };
1109
+ const args = {
1110
+ platform: "android",
1111
+ language: "ko-KR",
1112
+ dryRun: false,
1113
+ skipBuild: false,
1114
+ setupJenkins: false,
1115
+ setupGithub: false,
1116
+ setupGitlab: false,
1117
+ ci: "auto",
1118
+ ref: "main"
1119
+ };
967
1120
  for (let i = 0; i < argv.length; i++) {
968
1121
  if ((argv[i] === "--platform" || argv[i] === "-p") && argv[i + 1]) args.platform = argv[++i];
969
1122
  if (argv[i] === "--app" && argv[i + 1]) args.appId = argv[++i];
@@ -973,7 +1126,12 @@ function parseArgs4(argv) {
973
1126
  if (argv[i] === "--language" && argv[i + 1]) args.language = argv[++i];
974
1127
  if (argv[i] === "--dry-run") args.dryRun = true;
975
1128
  if (argv[i] === "--skip-build") args.skipBuild = true;
1129
+ if (argv[i] === "--ci" && argv[i + 1]) args.ci = argv[++i];
1130
+ if (argv[i] === "--workflow" && argv[i + 1]) args.workflow = argv[++i];
1131
+ if (argv[i] === "--ref" && argv[i + 1]) args.ref = argv[++i];
976
1132
  if (argv[i] === "setup-jenkins") args.setupJenkins = true;
1133
+ if (argv[i] === "setup-github") args.setupGithub = true;
1134
+ if (argv[i] === "setup-gitlab") args.setupGitlab = true;
977
1135
  }
978
1136
  return args;
979
1137
  }
@@ -989,6 +1147,78 @@ async function promptJenkinsSetup() {
989
1147
  rl.close();
990
1148
  return { url, user, token, jobAndroid: jobAndroid || void 0, jobIos: jobIos || void 0 };
991
1149
  }
1150
+ async function promptGitProviderSetup(provider) {
1151
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1152
+ const ask = (q) => new Promise((resolve) => rl.question(q, (a) => resolve(a.trim())));
1153
+ const isGh = provider === "github";
1154
+ log2(kleur6.bold(isGh ? "GitHub Actions \uC124\uC815" : "GitLab CI \uC124\uC815"));
1155
+ const tokenLabel = isGh ? " GitHub Personal Access Token (repo+workflow \uC2A4\uCF54\uD504): " : " GitLab Personal Access Token: ";
1156
+ const token = await ask(tokenLabel);
1157
+ const owner = await ask(isGh ? " Owner (org/user): " : " Namespace/group: ");
1158
+ const repo = await ask(" Repo \uC774\uB984 (\uACBD\uB85C \uC5C6\uC774): ");
1159
+ const hostPrompt = isGh ? " GitHub Enterprise host (\uC120\uD0DD, \uC5D4\uD130=github.com): " : " GitLab self-hosted URL (\uC120\uD0DD, \uC5D4\uD130=gitlab.com): ";
1160
+ const host = await ask(hostPrompt);
1161
+ rl.close();
1162
+ return {
1163
+ provider,
1164
+ token,
1165
+ owner,
1166
+ repo,
1167
+ host: host || void 0
1168
+ };
1169
+ }
1170
+ function resolveCi(ciOption, jenkins, ciProvider) {
1171
+ if (ciOption !== "auto") return ciOption;
1172
+ if (jenkins?.url && jenkins.token) return "jenkins";
1173
+ if (ciProvider) return ciProvider.provider;
1174
+ throw new Error(
1175
+ "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"
1176
+ );
1177
+ }
1178
+ async function runGitProviderBuild(cfg, args) {
1179
+ let runUrl = "";
1180
+ let runId;
1181
+ if (cfg.provider === "github") {
1182
+ if (!args.workflow) {
1183
+ throw new Error("--workflow \uD544\uC694 (\uC608: --workflow deploy.yml)");
1184
+ }
1185
+ log2(`\u{1F528} GitHub Actions \uD2B8\uB9AC\uAC70: ${kleur6.cyan(args.workflow)} @ ${args.ref}`);
1186
+ const inputs = {};
1187
+ if (args.appId) inputs.MIMI_APP_ID = args.appId;
1188
+ inputs.PLATFORM = args.platform;
1189
+ const result2 = await ghTriggerWorkflow(cfg, args.workflow, args.ref, inputs);
1190
+ if (!result2) {
1191
+ throw new Error("GitHub Actions run_id \uC870\uD68C \uC2E4\uD328. \uC7A0\uC2DC \uD6C4 ci_list_recent_builds \uB85C \uD655\uC778\uD558\uC138\uC694.");
1192
+ }
1193
+ runId = result2.runId;
1194
+ runUrl = result2.url;
1195
+ log2(kleur6.dim(` Run ID: ${runId} \u2192 ${runUrl}`));
1196
+ } else {
1197
+ log2(`\u{1F528} GitLab Pipeline \uD2B8\uB9AC\uAC70: ${args.ref}`);
1198
+ const variables = { PLATFORM: args.platform };
1199
+ if (args.appId) variables.MIMI_APP_ID = args.appId;
1200
+ const result2 = await glTriggerPipeline(cfg, args.ref, variables);
1201
+ runId = result2.pipelineId;
1202
+ runUrl = result2.url;
1203
+ log2(kleur6.dim(` Pipeline ID: ${runId} \u2192 ${runUrl}`));
1204
+ }
1205
+ log2(" \uC644\uB8CC \uB300\uAE30 \uC911...");
1206
+ let dots = 0;
1207
+ const onTick = (status) => {
1208
+ dots = (dots + 1) % 4;
1209
+ process.stdout.write(`\r \u23F3 ${status}${".".repeat(dots + 1)} `);
1210
+ };
1211
+ const result = cfg.provider === "github" ? await ghPollRun(cfg, runId, onTick) : await glPollPipeline(cfg, runId, onTick);
1212
+ process.stdout.write("\n");
1213
+ if (result === "success") {
1214
+ log2(kleur6.green(`\u2705 \uBE4C\uB4DC #${runId} \uC131\uACF5`));
1215
+ return runId;
1216
+ }
1217
+ log2(kleur6.red(`\uBE4C\uB4DC \uC885\uB8CC: ${result}`));
1218
+ log2(kleur6.dim(` ${runUrl}`));
1219
+ log2(kleur6.dim(` \uBE4C\uB4DC\uAC00 \uC774\uBBF8 \uC644\uB8CC\uB410\uB2E4\uBA74: mimi-seed deploy --skip-build --version-code <N> --platform ${args.platform}`));
1220
+ process.exit(1);
1221
+ }
992
1222
  async function cmdDeploy(argv) {
993
1223
  const args = parseArgs4(argv);
994
1224
  const cfg = await getEffectiveConfig();
@@ -1002,56 +1232,85 @@ async function cmdDeploy(argv) {
1002
1232
  log2(kleur6.green("\u2705 Jenkins \uC124\uC815 \uC800\uC7A5\uB428"));
1003
1233
  return;
1004
1234
  }
1235
+ if (args.setupGithub) {
1236
+ const ciCfg = await promptGitProviderSetup("github");
1237
+ saveCiProviderConfig(ciCfg);
1238
+ log2(kleur6.green(`\u2705 GitHub Actions \uC124\uC815 \uC800\uC7A5\uB428 \u2192 ~/.mimi-seed/ci.json`));
1239
+ return;
1240
+ }
1241
+ if (args.setupGitlab) {
1242
+ const ciCfg = await promptGitProviderSetup("gitlab");
1243
+ saveCiProviderConfig(ciCfg);
1244
+ log2(kleur6.green(`\u2705 GitLab CI \uC124\uC815 \uC800\uC7A5\uB428 \u2192 ~/.mimi-seed/ci.json`));
1245
+ return;
1246
+ }
1005
1247
  log2(kleur6.bold(`mimi-seed deploy \u2014 ${args.platform}`));
1006
1248
  if (args.dryRun) log2(kleur6.yellow(" [dry-run \uBAA8\uB4DC] \uC2E4\uC81C \uBC30\uD3EC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4"));
1007
1249
  log2("");
1008
1250
  let versionCode = args.versionCode;
1009
1251
  if (!args.skipBuild) {
1010
- if (!cfg.jenkins?.url || !cfg.jenkins?.token) {
1011
- log2(kleur6.yellow("Jenkins \uC124\uC815 \uC5C6\uC74C. `mimi-seed deploy setup-jenkins` \uB85C \uC124\uC815\uD558\uAC70\uB098 --skip-build \uC0AC\uC6A9."));
1012
- log2(kleur6.dim(" \uB610\uB294 \uC11C\uBC84 /workspace/integrations\uC5D0\uC11C jenkins \uD504\uB85C\uBC14\uC774\uB354 \uB4F1\uB85D \uD6C4 \uC11C\uBC84\uC0AC\uC774\uB4DC \uD2B8\uB9AC\uAC70 \uAC00\uB2A5."));
1013
- process.exit(1);
1014
- }
1015
- const jenkins = cfg.jenkins;
1016
- const jobName = args.platform === "android" ? jenkins.jobAndroid : jenkins.jobIos;
1017
- if (!jobName) {
1018
- log2(kleur6.red(`${args.platform} Jenkins job\uC774 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. setup-jenkins \uC2E4\uD589.`));
1019
- process.exit(1);
1020
- }
1021
- log2(`\u{1F528} Jenkins \uBE4C\uB4DC \uD2B8\uB9AC\uAC70: ${kleur6.cyan(jobName)}`);
1022
- const buildParams = {};
1023
- if (args.appId) buildParams.MIMI_APP_ID = args.appId;
1024
- const queueItemId = await triggerBuild(jenkins, jobName, buildParams);
1025
- if (!queueItemId) {
1026
- log2(kleur6.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."));
1252
+ const ciProvider = loadCiProviderConfig();
1253
+ const kind = resolveCi(args.ci, cfg.jenkins, ciProvider);
1254
+ log2(kleur6.dim(` CI: ${kind}`));
1255
+ if (kind === "jenkins") {
1256
+ if (!cfg.jenkins?.url || !cfg.jenkins?.token) {
1257
+ log2(kleur6.yellow("Jenkins \uC124\uC815 \uC5C6\uC74C. `mimi-seed deploy setup-jenkins` \uB85C \uC124\uC815\uD558\uAC70\uB098 --skip-build \uC0AC\uC6A9."));
1258
+ process.exit(1);
1259
+ }
1260
+ const jenkins = cfg.jenkins;
1261
+ const jobName = args.platform === "android" ? jenkins.jobAndroid : jenkins.jobIos;
1262
+ if (!jobName) {
1263
+ log2(kleur6.red(`${args.platform} Jenkins job\uC774 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. setup-jenkins \uC2E4\uD589.`));
1264
+ process.exit(1);
1265
+ }
1266
+ log2(`\u{1F528} Jenkins \uBE4C\uB4DC \uD2B8\uB9AC\uAC70: ${kleur6.cyan(jobName)}`);
1267
+ const buildParams = {};
1268
+ if (args.appId) buildParams.MIMI_APP_ID = args.appId;
1269
+ const queueItemId = await triggerBuild(jenkins, jobName, buildParams);
1270
+ if (!queueItemId) {
1271
+ log2(kleur6.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."));
1272
+ } else {
1273
+ log2(kleur6.dim(` Queue item: ${queueItemId}`));
1274
+ }
1275
+ let buildNumber = null;
1276
+ if (queueItemId) {
1277
+ log2(" \uBE4C\uB4DC \uBC88\uD638 \uB300\uAE30 \uC911...");
1278
+ for (let i = 0; i < 6; i++) {
1279
+ await new Promise((r) => setTimeout(r, 5e3));
1280
+ buildNumber = await getQueueBuildNumber(jenkins, queueItemId).catch(() => null);
1281
+ if (buildNumber) break;
1282
+ }
1283
+ }
1284
+ if (!buildNumber) {
1285
+ log2(kleur6.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."));
1286
+ process.exit(1);
1287
+ }
1288
+ log2(` \uBE4C\uB4DC #${buildNumber} \uC2DC\uC791\uB428. \uC644\uB8CC \uB300\uAE30 \uC911...`);
1289
+ const result = await pollBuildComplete(jenkins, jobName, buildNumber);
1290
+ if (result !== "SUCCESS") {
1291
+ log2(kleur6.red(`\uBE4C\uB4DC \uC2E4\uD328: ${result}`));
1292
+ log2(kleur6.dim(` Jenkins: ${jenkins.url}/job/${encodeURIComponent(jobName)}/${buildNumber}/`));
1293
+ log2(kleur6.dim(` \uBE4C\uB4DC\uAC00 \uC774\uBBF8 \uC644\uB8CC\uB410\uB2E4\uBA74: mimi-seed deploy --skip-build --version-code ${buildNumber} --platform ${args.platform}`));
1294
+ process.exit(1);
1295
+ }
1296
+ log2(kleur6.green(`\u2705 \uBE4C\uB4DC #${buildNumber} \uC131\uACF5`));
1297
+ if (!versionCode) {
1298
+ versionCode = buildNumber;
1299
+ log2(kleur6.dim(` versionCode = buildNumber (${versionCode})`));
1300
+ }
1027
1301
  } else {
1028
- log2(kleur6.dim(` Queue item: ${queueItemId}`));
1029
- }
1030
- let buildNumber = null;
1031
- if (queueItemId) {
1032
- log2(" \uBE4C\uB4DC \uBC88\uD638 \uB300\uAE30 \uC911...");
1033
- for (let i = 0; i < 6; i++) {
1034
- await new Promise((r) => setTimeout(r, 5e3));
1035
- buildNumber = await getQueueBuildNumber(jenkins, queueItemId).catch(() => null);
1036
- if (buildNumber) break;
1302
+ if (!ciProvider) {
1303
+ log2(kleur6.red(`${kind} \uC124\uC815\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. setup-${kind} \uC2E4\uD589.`));
1304
+ process.exit(1);
1305
+ }
1306
+ const buildId = await runGitProviderBuild(ciProvider, args);
1307
+ if (!versionCode) {
1308
+ log2(kleur6.red(`\u2717 versionCode \uBBF8\uC9C0\uC815 (${kind} run_id ${buildId}\uB294 versionCode\uB85C \uBD80\uC801\uD569)`));
1309
+ log2(kleur6.dim(" \uAD8C\uC7A5 \uC0AC\uD56D:"));
1310
+ log2(kleur6.dim(" \u2022 CI \uC6CC\uD06C\uD50C\uB85C \uC548\uC5D0\uC11C versionCode\uB97C \uACB0\uC815\uD558\uACE0 \uACB0\uACFC\uB97C \uCD9C\uB825"));
1311
+ log2(kleur6.dim(" \u2022 \uB2E4\uC74C \uC2E4\uD589: mimi-seed deploy --skip-build --version-code <N>"));
1312
+ process.exit(1);
1037
1313
  }
1038
- }
1039
- if (!buildNumber) {
1040
- log2(kleur6.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."));
1041
- process.exit(1);
1042
- }
1043
- log2(` \uBE4C\uB4DC #${buildNumber} \uC2DC\uC791\uB428. \uC644\uB8CC \uB300\uAE30 \uC911...`);
1044
- const result = await pollBuildComplete(jenkins, jobName, buildNumber);
1045
- if (result !== "SUCCESS") {
1046
- log2(kleur6.red(`\uBE4C\uB4DC \uC2E4\uD328: ${result}`));
1047
- log2(kleur6.dim(` Jenkins: ${jenkins.url}/job/${encodeURIComponent(jobName)}/${buildNumber}/`));
1048
- log2(kleur6.dim(` \uBE4C\uB4DC\uAC00 \uC774\uBBF8 \uC644\uB8CC\uB410\uB2E4\uBA74: mimi-seed deploy --skip-build --version-code ${buildNumber} --platform ${args.platform}`));
1049
- process.exit(1);
1050
- }
1051
- log2(kleur6.green(`\u2705 \uBE4C\uB4DC #${buildNumber} \uC131\uACF5`));
1052
- if (!versionCode) {
1053
- versionCode = buildNumber;
1054
- log2(kleur6.dim(` versionCode = buildNumber (${versionCode})`));
1055
1314
  }
1056
1315
  }
1057
1316
  if (!versionCode) {
@@ -1098,17 +1357,17 @@ async function cmdDeploy(argv) {
1098
1357
 
1099
1358
  // src/mcp-restart.ts
1100
1359
  import { execSync as execSync2 } from "child_process";
1101
- import fs3 from "fs";
1102
- import os2 from "os";
1103
- import path3 from "path";
1360
+ import fs4 from "fs";
1361
+ import os3 from "os";
1362
+ import path4 from "path";
1104
1363
  import kleur7 from "kleur";
1105
1364
  function log3(msg) {
1106
1365
  process.stdout.write(msg + "\n");
1107
1366
  }
1108
1367
  function readClaudeJson() {
1109
- const p = path3.join(os2.homedir(), ".claude.json");
1368
+ const p = path4.join(os3.homedir(), ".claude.json");
1110
1369
  try {
1111
- return JSON.parse(fs3.readFileSync(p, "utf8"));
1370
+ return JSON.parse(fs4.readFileSync(p, "utf8"));
1112
1371
  } catch {
1113
1372
  return {};
1114
1373
  }
@@ -1124,7 +1383,7 @@ function findProcessMarker(cfg) {
1124
1383
  return meaningful.at(-1) ?? null;
1125
1384
  }
1126
1385
  function killByMarker(marker) {
1127
- const isWin = os2.platform() === "win32";
1386
+ const isWin = os3.platform() === "win32";
1128
1387
  if (isWin) {
1129
1388
  const escaped = marker.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
1130
1389
  let pids = [];
@@ -1235,7 +1494,7 @@ async function cmdInit() {
1235
1494
  return;
1236
1495
  }
1237
1496
  log4("\u{1F510} \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uB85C\uADF8\uC778 \uB300\uAE30...");
1238
- const hostName = os3.hostname().slice(0, 32);
1497
+ const hostName = os4.hostname().slice(0, 32);
1239
1498
  const name = `cli-${hostName}`;
1240
1499
  const { port, promise } = await awaitHandshake(5 * 60 * 1e3);
1241
1500
  const callback = `http://127.0.0.1:${port}/cb`;
@@ -1271,10 +1530,10 @@ async function cmdInit() {
1271
1530
  }
1272
1531
  log4("");
1273
1532
  }
1274
- const claudeDir = path4.join(cwd, ".claude");
1275
- const agentMdPath = path4.join(claudeDir, "mimi-seed.md");
1276
- if (!fs4.existsSync(agentMdPath)) {
1277
- fs4.mkdirSync(claudeDir, { recursive: true });
1533
+ const claudeDir = path5.join(cwd, ".claude");
1534
+ const agentMdPath = path5.join(claudeDir, "mimi-seed.md");
1535
+ if (!fs5.existsSync(agentMdPath)) {
1536
+ fs5.mkdirSync(claudeDir, { recursive: true });
1278
1537
  const appLines = hints.flatMap((h) => {
1279
1538
  const parts = [
1280
1539
  h.name && ` name: ${h.name}`,
@@ -1305,7 +1564,7 @@ async function cmdInit() {
1305
1564
  "- `/mimi-seed:health` \u2014 \uC5F0\uACB0 \uC0C1\uD0DC \uBE60\uB978 \uD655\uC778",
1306
1565
  "- `/mimi-seed:review-inbox` \u2014 \uBBF8\uB2F5\uBCC0 \uB9AC\uBDF0 \uB2F5\uBCC0"
1307
1566
  ].join("\n");
1308
- fs4.writeFileSync(agentMdPath, agentMd, { mode: 420 });
1567
+ fs5.writeFileSync(agentMdPath, agentMd, { mode: 420 });
1309
1568
  log4(kleur8.dim(` \uC5D0\uC774\uC804\uD2B8 \uC124\uC815: .claude/mimi-seed.md`));
1310
1569
  }
1311
1570
  log4(kleur8.bold("\u2713 \uC900\uBE44 \uC644\uB8CC."));
@@ -1387,15 +1646,20 @@ ${kleur8.bold("mimi-seed review \uC635\uC158:")}
1387
1646
  --no-interactive CI \uBAA8\uB4DC
1388
1647
 
1389
1648
  ${kleur8.bold("mimi-seed deploy \uC635\uC158:")}
1390
- --platform android|ios \uBC30\uD3EC \uD50C\uB7AB\uD3FC (\uAE30\uBCF8: android)
1391
- --app <id> \uC571 ID \uC9C0\uC815
1392
- --version-code <n> \uBE4C\uB4DC \uBC88\uD638 \uC9C1\uC811 \uC9C0\uC815 (--skip-build \uC640 \uD568\uAED8)
1393
- --from <ref> \uCEE4\uBC0B \uBC94\uC704 \uC2DC\uC791 (\uB9B4\uB9AC\uC988 \uB178\uD2B8\uC6A9)
1394
- --to <ref> \uCEE4\uBC0B \uBC94\uC704 \uB05D (\uAE30\uBCF8: HEAD)
1395
- --language <\uCF54\uB4DC> \uB9B4\uB9AC\uC988 \uB178\uD2B8 \uC5B8\uC5B4 (\uAE30\uBCF8: ko-KR)
1396
- --dry-run \uC2E4\uC81C \uBC30\uD3EC \uC5C6\uC774 \uD30C\uC774\uD504\uB77C\uC778 \uD14C\uC2A4\uD2B8
1397
- --skip-build Jenkins \uBE4C\uB4DC \uAC74\uB108\uB700 (--version-code \uD544\uC218)
1398
- setup-jenkins Jenkins \uC124\uC815 \uB300\uD654\uD615 \uB4F1\uB85D
1649
+ --platform android|ios \uBC30\uD3EC \uD50C\uB7AB\uD3FC (\uAE30\uBCF8: android)
1650
+ --app <id> \uC571 ID \uC9C0\uC815
1651
+ --version-code <n> \uBE4C\uB4DC \uBC88\uD638 \uC9C1\uC811 \uC9C0\uC815 (--skip-build \uC640 \uD568\uAED8)
1652
+ --from <ref> \uCEE4\uBC0B \uBC94\uC704 \uC2DC\uC791 (\uB9B4\uB9AC\uC988 \uB178\uD2B8\uC6A9)
1653
+ --to <ref> \uCEE4\uBC0B \uBC94\uC704 \uB05D (\uAE30\uBCF8: HEAD)
1654
+ --language <\uCF54\uB4DC> \uB9B4\uB9AC\uC988 \uB178\uD2B8 \uC5B8\uC5B4 (\uAE30\uBCF8: ko-KR)
1655
+ --dry-run \uC2E4\uC81C \uBC30\uD3EC \uC5C6\uC774 \uD30C\uC774\uD504\uB77C\uC778 \uD14C\uC2A4\uD2B8
1656
+ --skip-build CI \uBE4C\uB4DC \uAC74\uB108\uB700 (--version-code \uD544\uC218)
1657
+ --ci jenkins|github|gitlab CI \uAC15\uC81C \uC120\uD0DD (\uAE30\uBCF8: auto)
1658
+ --workflow <file> GitHub workflow \uD30C\uC77C (\uC608: deploy.yml)
1659
+ --ref <branch|tag> GitHub/GitLab \uBE0C\uB79C\uCE58/\uD0DC\uADF8 (\uAE30\uBCF8: main)
1660
+ setup-jenkins Jenkins \uC124\uC815 \uB300\uD654\uD615 \uB4F1\uB85D
1661
+ setup-github GitHub Actions \uC124\uC815 \uB300\uD654\uD615 \uB4F1\uB85D
1662
+ setup-gitlab GitLab CI \uC124\uC815 \uB300\uD654\uD615 \uB4F1\uB85D
1399
1663
 
1400
1664
  ${kleur8.bold("\uD658\uACBD\uBCC0\uC218:")}
1401
1665
  MIMI_SEED_TOKEN PAT \uD1A0\uD070 (CI/CD \uBB34\uC778\uC99D \uBAA8\uB4DC)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mimi-seed",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "Mimi Seed CLI — Claude Code에서 앱 출시 운영을 관리합니다.",
5
5
  "bin": {
6
6
  "mimi-seed": "dist/index.js"
@@ -13,7 +13,9 @@
13
13
  ],
14
14
  "scripts": {
15
15
  "build": "tsup",
16
- "dev": "tsx src/index.ts"
16
+ "dev": "tsx src/index.ts",
17
+ "test": "vitest run",
18
+ "test:watch": "vitest"
17
19
  },
18
20
  "engines": {
19
21
  "node": ">=18"
@@ -27,7 +29,8 @@
27
29
  "@types/node": "^20.11.0",
28
30
  "tsup": "^8.0.0",
29
31
  "tsx": "^4.7.0",
30
- "typescript": "^5.4.0"
32
+ "typescript": "^5.4.0",
33
+ "vitest": "^4.1.5"
31
34
  },
32
35
  "publishConfig": {
33
36
  "access": "public"