mimi-seed 0.19.17 → 0.19.18

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.
@@ -1231,6 +1231,7 @@ async function hasAnyProjectSignal(cwd) {
1231
1231
  // src/deploy.ts
1232
1232
  import kleur from "kleur";
1233
1233
  import * as readline from "readline";
1234
+ import { randomUUID } from "crypto";
1234
1235
 
1235
1236
  // src/config.ts
1236
1237
  import fs6 from "fs/promises";
@@ -1272,10 +1273,234 @@ async function getEffectiveConfig() {
1272
1273
  return readConfig();
1273
1274
  }
1274
1275
 
1275
- // src/jenkins-project.ts
1276
- import fs7 from "fs";
1276
+ // src/deploy-stream.ts
1277
+ var DeployStreamError = class extends Error {
1278
+ constructor(reason, message = "") {
1279
+ super(message);
1280
+ this.reason = reason;
1281
+ }
1282
+ reason;
1283
+ };
1284
+ async function consumeDeployStream(reader, onEvent) {
1285
+ const decoder = new TextDecoder("utf-8", { fatal: true });
1286
+ let line = "";
1287
+ let data = [];
1288
+ let skipLf = false;
1289
+ let done = false;
1290
+ let jobId;
1291
+ function dispatch() {
1292
+ if (data.length === 0) return;
1293
+ const payload = data.join("\n");
1294
+ data = [];
1295
+ let value;
1296
+ try {
1297
+ value = JSON.parse(payload);
1298
+ } catch {
1299
+ throw new DeployStreamError("malformed");
1300
+ }
1301
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1302
+ throw new DeployStreamError("malformed");
1303
+ }
1304
+ const event = value;
1305
+ if (typeof event.phase !== "string" || !event.phase || typeof event.message !== "string" || !["running", "done", "failed", "skipped"].includes(String(event.status)) || event.jobId !== void 0 && (typeof event.jobId !== "string" || !event.jobId)) {
1306
+ throw new DeployStreamError("malformed");
1307
+ }
1308
+ const parsed = event;
1309
+ if (parsed.jobId) jobId = parsed.jobId;
1310
+ onEvent(parsed);
1311
+ if (parsed.status === "failed") throw new DeployStreamError("failed", parsed.message);
1312
+ if (parsed.phase === "done" && parsed.status === "done") done = true;
1313
+ }
1314
+ function consumeLine() {
1315
+ if (!line) {
1316
+ dispatch();
1317
+ } else if (!line.startsWith(":")) {
1318
+ const colon = line.indexOf(":");
1319
+ const field = colon < 0 ? line : line.slice(0, colon);
1320
+ const raw = colon < 0 ? "" : line.slice(colon + 1);
1321
+ if (field === "data") data.push(raw.startsWith(" ") ? raw.slice(1) : raw);
1322
+ }
1323
+ line = "";
1324
+ }
1325
+ function consumeText(text) {
1326
+ for (const char of text) {
1327
+ if (skipLf) {
1328
+ skipLf = false;
1329
+ if (char === "\n") continue;
1330
+ }
1331
+ if (char === "\r") {
1332
+ consumeLine();
1333
+ skipLf = true;
1334
+ } else if (char === "\n") {
1335
+ consumeLine();
1336
+ } else {
1337
+ line += char;
1338
+ }
1339
+ }
1340
+ }
1341
+ try {
1342
+ while (true) {
1343
+ const { done: eof, value } = await reader.read();
1344
+ if (eof) break;
1345
+ try {
1346
+ consumeText(decoder.decode(value, { stream: true }));
1347
+ } catch (error) {
1348
+ if (error instanceof TypeError) throw new DeployStreamError("malformed");
1349
+ throw error;
1350
+ }
1351
+ }
1352
+ try {
1353
+ consumeText(decoder.decode());
1354
+ } catch (error) {
1355
+ if (error instanceof TypeError) throw new DeployStreamError("malformed");
1356
+ throw error;
1357
+ }
1358
+ } finally {
1359
+ reader.releaseLock();
1360
+ }
1361
+ if (line) consumeLine();
1362
+ dispatch();
1363
+ if (!done) throw new DeployStreamError("incomplete");
1364
+ return jobId;
1365
+ }
1366
+
1367
+ // src/project-link.ts
1368
+ import fs7 from "fs/promises";
1277
1369
  import path8 from "path";
1370
+ var PROJECT_LINK_FILENAME = ".mimi-seed-link.json";
1278
1371
  var M = catalog({
1372
+ lookupFailed: (status) => `\uD504\uB85C\uC81D\uD2B8 \uC571 \uC5F0\uACB0 \uC870\uD68C \uC2E4\uD328 (${status}). --app <id>\uB85C \uC9C0\uC815\uD558\uC138\uC694.`,
1373
+ invalidResponse: "\uD504\uB85C\uC81D\uD2B8 \uC571 \uC5F0\uACB0 \uC751\uB2F5\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
1374
+ mismatch: "\uC800\uC7A5\uB41C \uD504\uB85C\uC81D\uD2B8 \uC571 \uC5F0\uACB0\uC774 \uD604\uC7AC \uACC4\uC815\xB7\uC571\uACFC \uB2E4\uB985\uB2C8\uB2E4. \uC5F0\uACB0 \uD30C\uC77C\uC744 \uD655\uC778\uD558\uC138\uC694.",
1375
+ invalidFile: "\uD504\uB85C\uC81D\uD2B8 \uC571 \uC5F0\uACB0 \uD30C\uC77C\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4."
1376
+ }, {
1377
+ lookupFailed: (status) => `Project app lookup failed (${status}). Specify --app <id>.`,
1378
+ invalidResponse: "Invalid project app lookup response.",
1379
+ mismatch: "Saved project app link differs from the current account or app. Check the link file.",
1380
+ invalidFile: "Invalid project app link file."
1381
+ });
1382
+ function base(value) {
1383
+ return value.replace(/\/+$/, "");
1384
+ }
1385
+ function isLink(value) {
1386
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
1387
+ const link = value;
1388
+ return link.schema === 1 && typeof link.webBase === "string" && !!link.webBase && typeof link.appId === "string" && !!link.appId && (link.packageName === void 0 || typeof link.packageName === "string") && (link.bundleId === void 0 || typeof link.bundleId === "string");
1389
+ }
1390
+ async function findProjectLink(cwd) {
1391
+ let dir = path8.resolve(cwd);
1392
+ for (let depth = 0; depth <= 8; depth++) {
1393
+ const file = path8.join(dir, PROJECT_LINK_FILENAME);
1394
+ try {
1395
+ const parsed = JSON.parse(await fs7.readFile(file, "utf8"));
1396
+ if (!isLink(parsed)) throw new Error(M().invalidFile);
1397
+ return parsed;
1398
+ } catch (error) {
1399
+ if (error.code !== "ENOENT") throw error;
1400
+ }
1401
+ const parent = path8.dirname(dir);
1402
+ if (parent === dir) break;
1403
+ dir = parent;
1404
+ }
1405
+ return null;
1406
+ }
1407
+ function resolveLinkedAppId(link, webBase, explicitAppId) {
1408
+ if (!link) return explicitAppId;
1409
+ if (base(link.webBase) !== base(webBase) || explicitAppId && explicitAppId !== link.appId) {
1410
+ throw new Error(M().mismatch);
1411
+ }
1412
+ return link.appId;
1413
+ }
1414
+ function validateProjectIdentity(link, hints) {
1415
+ if (!link || hints.length === 0) return;
1416
+ if (!hints.every((hint) => (!hint.packageName || hint.packageName === link.packageName) && (!hint.bundleId || hint.bundleId === link.bundleId))) {
1417
+ throw new Error(M().mismatch);
1418
+ }
1419
+ }
1420
+ async function linkProject(cwd, webBase, token, hint) {
1421
+ const res = await fetch(new URL("/api/deploy/link", webBase), {
1422
+ method: "POST",
1423
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
1424
+ body: JSON.stringify({ packageName: hint.packageName, bundleId: hint.bundleId }),
1425
+ signal: AbortSignal.timeout(3e4)
1426
+ });
1427
+ if (!res.ok) throw new Error(M().lookupFailed(res.status));
1428
+ const value = await res.json();
1429
+ const app = value && typeof value === "object" && "app" in value ? value.app : null;
1430
+ if (!app || typeof app !== "object" || !("id" in app) || typeof app.id !== "string" || !app.id || hint.packageName && (!("packageName" in app) || app.packageName !== hint.packageName) || hint.bundleId && (!("bundleId" in app) || app.bundleId !== hint.bundleId)) {
1431
+ throw new Error(M().invalidResponse);
1432
+ }
1433
+ const link = {
1434
+ schema: 1,
1435
+ webBase: base(webBase),
1436
+ appId: app.id,
1437
+ ...hint.packageName ? { packageName: hint.packageName } : {},
1438
+ ...hint.bundleId ? { bundleId: hint.bundleId } : {}
1439
+ };
1440
+ const existing = await findProjectLink(cwd);
1441
+ if (existing && (existing.appId !== link.appId || base(existing.webBase) !== link.webBase || existing.packageName !== link.packageName || existing.bundleId !== link.bundleId)) {
1442
+ throw new Error(M().mismatch);
1443
+ }
1444
+ if (!existing) {
1445
+ await fs7.writeFile(path8.join(cwd, PROJECT_LINK_FILENAME), `${JSON.stringify(link, null, 2)}
1446
+ `, { mode: 384, flag: "wx" });
1447
+ }
1448
+ const ignorePath = path8.join(cwd, ".gitignore");
1449
+ const ignore = await fs7.readFile(ignorePath, "utf8").catch((error) => {
1450
+ if (error.code !== "ENOENT") throw error;
1451
+ return "";
1452
+ });
1453
+ if (!ignore.split(/\r?\n/).includes(PROJECT_LINK_FILENAME)) {
1454
+ await fs7.appendFile(ignorePath, `${ignore && !ignore.endsWith("\n") ? "\n" : ""}${PROJECT_LINK_FILENAME}
1455
+ `);
1456
+ }
1457
+ return link;
1458
+ }
1459
+
1460
+ // src/deploy-runs.ts
1461
+ function parseRun(value) {
1462
+ if (!value || typeof value !== "object") throw new Error(M2().invalidResponse);
1463
+ const run = value;
1464
+ const jobId = run.jobId ?? run.id;
1465
+ if (typeof jobId !== "string" || !jobId || typeof run.appId !== "string" || !run.appId || run.platform !== "android" && run.platform !== "ios" || typeof run.status !== "string" || run.versionCode != null && (!Number.isSafeInteger(run.versionCode) || run.versionCode < 1) || run.jenkinsBuildNumber != null && (!Number.isSafeInteger(run.jenkinsBuildNumber) || run.jenkinsBuildNumber < 1) || run.fromRef != null && typeof run.fromRef !== "string" || run.toRef != null && typeof run.toRef !== "string") {
1466
+ throw new Error(M2().invalidResponse);
1467
+ }
1468
+ return { ...run, jobId };
1469
+ }
1470
+ async function updateDeployRun(webBase, token, request) {
1471
+ const res = await fetch(new URL("/api/deploy/runs", webBase), {
1472
+ method: "POST",
1473
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
1474
+ body: JSON.stringify(request),
1475
+ signal: AbortSignal.timeout(3e4)
1476
+ });
1477
+ if (!res.ok) throw new Error(M2().updateFailed(res.status));
1478
+ return parseRun(await res.json());
1479
+ }
1480
+ async function getDeployRun(webBase, token, runId) {
1481
+ const url = new URL("/api/deploy", webBase);
1482
+ url.searchParams.set("jobId", runId);
1483
+ const res = await fetch(url, {
1484
+ headers: { Authorization: `Bearer ${token}` },
1485
+ signal: AbortSignal.timeout(3e4)
1486
+ });
1487
+ if (!res.ok) throw new Error(M2().lookupFailed(res.status));
1488
+ return parseRun(await res.json());
1489
+ }
1490
+ var M2 = catalog({
1491
+ invalidResponse: "\uBC30\uD3EC \uAE30\uB85D \uC751\uB2F5\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
1492
+ updateFailed: (status) => `\uBC30\uD3EC \uAE30\uB85D \uAC31\uC2E0 \uC2E4\uD328 (${status}). \uC6F9 \uCF58\uC194\uC5D0\uC11C \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uC138\uC694.`,
1493
+ lookupFailed: (status) => `\uBC30\uD3EC \uAE30\uB85D \uC870\uD68C \uC2E4\uD328 (${status}). \uC6F9 \uCF58\uC194\uC5D0\uC11C \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uC138\uC694.`
1494
+ }, {
1495
+ invalidResponse: "Invalid deployment run response.",
1496
+ updateFailed: (status) => `Deployment record update failed (${status}). Check its status in the web console.`,
1497
+ lookupFailed: (status) => `Deployment record lookup failed (${status}). Check its status in the web console.`
1498
+ });
1499
+
1500
+ // src/jenkins-project.ts
1501
+ import fs8 from "fs";
1502
+ import path9 from "path";
1503
+ var M3 = catalog({
1279
1504
  invalidManifest: "\uD504\uB85C\uC81D\uD2B8 .mimi-seed.json\uC740 \uC62C\uBC14\uB978 JSON \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4.",
1280
1505
  controllerMismatch: "\uD504\uB85C\uC81D\uD2B8 Jenkins URL\uACFC \uB85C\uCEEC\uC5D0\uC11C \uC778\uC99D\uB41C \uCEE8\uD2B8\uB864\uB7EC\uAC00 \uB2E4\uB985\uB2C8\uB2E4.",
1281
1506
  invalidJob: ".mimi-seed.json\uC758 services.jenkins.jobAndroid/jobIos\uC5D0 \uC62C\uBC14\uB978 \uC7A1 \uACBD\uB85C\uB97C \uC124\uC815\uD558\uC138\uC694.",
@@ -1288,26 +1513,26 @@ var M = catalog({
1288
1513
  });
1289
1514
  function resolveProjectJenkins(cfg, platform, cwd = process.cwd()) {
1290
1515
  const field = platform === "android" ? "jobAndroid" : "jobIos";
1291
- let dir = path8.resolve(cwd);
1516
+ let dir = path9.resolve(cwd);
1292
1517
  for (let depth = 0; depth <= 8; depth++) {
1293
- const file = path8.join(dir, MANIFEST_FILENAME);
1294
- if (fs7.existsSync(file)) {
1518
+ const file = path9.join(dir, MANIFEST_FILENAME);
1519
+ if (fs8.existsSync(file)) {
1295
1520
  let manifest;
1296
1521
  try {
1297
- manifest = JSON.parse(fs7.readFileSync(file, "utf8"));
1522
+ manifest = JSON.parse(fs8.readFileSync(file, "utf8"));
1298
1523
  } catch {
1299
- throw new Error(M().invalidManifest);
1524
+ throw new Error(M3().invalidManifest);
1300
1525
  }
1301
1526
  if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
1302
- throw new Error(M().invalidManifest);
1527
+ throw new Error(M3().invalidManifest);
1303
1528
  }
1304
1529
  const service = manifest.services?.jenkins;
1305
1530
  if (service?.url !== void 0 && (typeof service.url !== "string" || service.url.replace(/\/+$/, "") !== cfg.url.replace(/\/+$/, ""))) {
1306
- throw new Error(M().controllerMismatch);
1531
+ throw new Error(M3().controllerMismatch);
1307
1532
  }
1308
1533
  return { job: validateJob(service?.[field]), source: file };
1309
1534
  }
1310
- const parent = path8.dirname(dir);
1535
+ const parent = path9.dirname(dir);
1311
1536
  if (parent === dir) break;
1312
1537
  dir = parent;
1313
1538
  }
@@ -1315,7 +1540,7 @@ function resolveProjectJenkins(cfg, platform, cwd = process.cwd()) {
1315
1540
  }
1316
1541
  function validateJob(job) {
1317
1542
  if (typeof job !== "string" || !job || job.split("/").some((part) => !part || part === "." || part === ".." || /[\\?#%]/.test(part) || [...part].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))) {
1318
- throw new Error(M().invalidJob);
1543
+ throw new Error(M3().invalidJob);
1319
1544
  }
1320
1545
  return job;
1321
1546
  }
@@ -1324,7 +1549,7 @@ function jenkinsJobPath(job) {
1324
1549
  }
1325
1550
  function jenkinsBuildParameters(platform, ref, appId) {
1326
1551
  if (!ref || /^[a-f0-9]{7,40}$/i.test(ref) || ref.startsWith("-") || /[\s~^:?*[\\]|\.\.|@\{/.test(ref)) {
1327
- throw new Error(M().invalidRef);
1552
+ throw new Error(M3().invalidRef);
1328
1553
  }
1329
1554
  return {
1330
1555
  BUILD_TARGET: platform,
@@ -1340,11 +1565,11 @@ function jenkinsBuildParameters(platform, ref, appId) {
1340
1565
  }
1341
1566
 
1342
1567
  // src/ci-providers.ts
1343
- import fs8 from "fs";
1344
- import path9 from "path";
1568
+ import fs9 from "fs";
1569
+ import path10 from "path";
1345
1570
  import os5 from "os";
1346
- var CI_CONFIG_PATH = path9.join(os5.homedir(), ".mimi-seed", "ci.json");
1347
- var M2 = catalog(
1571
+ var CI_CONFIG_PATH = path10.join(os5.homedir(), ".mimi-seed", "ci.json");
1572
+ var M4 = catalog(
1348
1573
  {
1349
1574
  badToken: (provider, status) => `${provider} ${status} \u2014 \uD1A0\uD070\uC774 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC544`,
1350
1575
  noScope: (scopes) => `\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.`,
@@ -1361,7 +1586,7 @@ var M2 = catalog(
1361
1586
  );
1362
1587
  function loadCiProviderConfig() {
1363
1588
  try {
1364
- const cfg = JSON.parse(fs8.readFileSync(CI_CONFIG_PATH, "utf-8"));
1589
+ const cfg = JSON.parse(fs9.readFileSync(CI_CONFIG_PATH, "utf-8"));
1365
1590
  const host = normalizeHost(cfg.host);
1366
1591
  return host ? { ...cfg, host } : { ...cfg, host: void 0 };
1367
1592
  } catch {
@@ -1375,15 +1600,15 @@ function normalizeHost(host) {
1375
1600
  return withScheme.replace(/\/+$/, "");
1376
1601
  }
1377
1602
  function saveCiProviderConfig(cfg) {
1378
- const dir = path9.dirname(CI_CONFIG_PATH);
1379
- if (!fs8.existsSync(dir)) {
1380
- fs8.mkdirSync(dir, { recursive: true, mode: 448 });
1603
+ const dir = path10.dirname(CI_CONFIG_PATH);
1604
+ if (!fs9.existsSync(dir)) {
1605
+ fs9.mkdirSync(dir, { recursive: true, mode: 448 });
1381
1606
  }
1382
1607
  const normalized = { ...cfg, host: normalizeHost(cfg.host) };
1383
1608
  if (!normalized.host) delete normalized.host;
1384
- fs8.writeFileSync(CI_CONFIG_PATH, JSON.stringify(normalized, null, 2));
1609
+ fs9.writeFileSync(CI_CONFIG_PATH, JSON.stringify(normalized, null, 2));
1385
1610
  if (process.platform !== "win32") {
1386
- fs8.chmodSync(CI_CONFIG_PATH, 384);
1611
+ fs9.chmodSync(CI_CONFIG_PATH, 384);
1387
1612
  }
1388
1613
  }
1389
1614
  async function verifyCiToken(cfg) {
@@ -1394,18 +1619,18 @@ async function verifyCiToken(cfg) {
1394
1619
  headers: { Authorization: `Bearer ${probe.token}`, Accept: "application/vnd.github+json" }
1395
1620
  });
1396
1621
  if (!res2.ok) {
1397
- return { ok: false, reason: M2().badToken("GitHub", res2.status) };
1622
+ return { ok: false, reason: M4().badToken("GitHub", res2.status) };
1398
1623
  }
1399
1624
  const user2 = await res2.json();
1400
1625
  const scopes = res2.headers.get("x-oauth-scopes");
1401
1626
  if (scopes !== null && !scopes.split(/,\s*/).filter(Boolean).includes("workflow")) {
1402
- return { ok: false, reason: M2().noScope(scopes) };
1627
+ return { ok: false, reason: M4().noScope(scopes) };
1403
1628
  }
1404
1629
  return { ok: true, login: user2.login };
1405
1630
  }
1406
1631
  const res = await fetch(`${glBase(probe)}/user`, { headers: { "PRIVATE-TOKEN": probe.token } });
1407
1632
  if (!res.ok) {
1408
- return { ok: false, reason: M2().badToken("GitLab", res.status) };
1633
+ return { ok: false, reason: M4().badToken("GitLab", res.status) };
1409
1634
  }
1410
1635
  const user = await res.json();
1411
1636
  return { ok: true, login: user.username };
@@ -1463,12 +1688,12 @@ async function ghPollRun(cfg, runId, onTick, timeoutMs = 30 * 60 * 1e3, interval
1463
1688
  );
1464
1689
  } catch {
1465
1690
  consecutiveErrors++;
1466
- if (consecutiveErrors >= 3) throw new Error(M2().pollFailed("GitHub"));
1691
+ if (consecutiveErrors >= 3) throw new Error(M4().pollFailed("GitHub"));
1467
1692
  continue;
1468
1693
  }
1469
1694
  if (!res.ok) {
1470
1695
  consecutiveErrors++;
1471
- if (consecutiveErrors >= 3) throw new Error(M2().pollFailedHttp("GitHub", res.status));
1696
+ if (consecutiveErrors >= 3) throw new Error(M4().pollFailedHttp("GitHub", res.status));
1472
1697
  continue;
1473
1698
  }
1474
1699
  consecutiveErrors = 0;
@@ -1514,12 +1739,12 @@ async function glPollPipeline(cfg, pipelineId, onTick, timeoutMs = 30 * 60 * 1e3
1514
1739
  );
1515
1740
  } catch {
1516
1741
  consecutiveErrors++;
1517
- if (consecutiveErrors >= 3) throw new Error(M2().pollFailed("GitLab"));
1742
+ if (consecutiveErrors >= 3) throw new Error(M4().pollFailed("GitLab"));
1518
1743
  continue;
1519
1744
  }
1520
1745
  if (!res.ok) {
1521
1746
  consecutiveErrors++;
1522
- if (consecutiveErrors >= 3) throw new Error(M2().pollFailedHttp("GitLab", res.status));
1747
+ if (consecutiveErrors >= 3) throw new Error(M4().pollFailedHttp("GitLab", res.status));
1523
1748
  continue;
1524
1749
  }
1525
1750
  consecutiveErrors = 0;
@@ -1545,7 +1770,7 @@ var PHASE_ICON = {
1545
1770
  function log(msg) {
1546
1771
  process.stdout.write(msg + "\n");
1547
1772
  }
1548
- var M3 = catalog(
1773
+ var M5 = catalog(
1549
1774
  {
1550
1775
  missingValue: (option) => `${option} \uB4A4\uC5D0 \uAC12\uC744 \uC785\uB825\uD558\uC138\uC694.`,
1551
1776
  unknownOption: (option) => `\uC54C \uC218 \uC5C6\uB294 deploy \uC635\uC158: ${option}`,
@@ -1564,6 +1789,15 @@ var M3 = catalog(
1564
1789
  // 서버 스트림
1565
1790
  serverDeployFailed: (status, body) => `\uC11C\uBC84 \uBC30\uD3EC \uC2E4\uD328 ${status}: ${body}`,
1566
1791
  noSseStream: "SSE \uC2A4\uD2B8\uB9BC \uC5C6\uC74C",
1792
+ malformedSse: "\uC11C\uBC84 \uBC30\uD3EC \uC751\uB2F5\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uC6F9 \uCF58\uC194\uC5D0\uC11C \uBC30\uD3EC \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uC138\uC694.",
1793
+ incompleteSse: "\uC11C\uBC84 \uBC30\uD3EC \uC751\uB2F5\uC774 \uC644\uB8CC \uC804\uC5D0 \uB04A\uACBC\uC2B5\uB2C8\uB2E4. \uC6F9 \uCF58\uC194\uC5D0\uC11C \uBC30\uD3EC \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uC138\uC694.",
1794
+ deploymentLink: (url) => ` \uC6F9 \uBC30\uD3EC \uAE30\uB85D: ${url}`,
1795
+ runId: (id) => ` \uBC30\uD3EC ID: ${id}`,
1796
+ prepared: (id) => `\uBC30\uD3EC \uC900\uBE44 \uC644\uB8CC. \uC81C\uCD9C\uD558\uB824\uBA74 mimi-seed deploy --resume ${id} --yes`,
1797
+ invalidResume: "--resume\uC740 --skip-build, --prepare-only, --dry-run\uACFC \uD568\uAED8 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
1798
+ resumeNotReady: "\uC900\uBE44 \uC644\uB8CC \uC0C1\uD0DC\uC758 \uBC30\uD3EC\uB9CC \uC7AC\uAC1C\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
1799
+ runMismatch: "\uAE30\uC874 \uBC30\uD3EC \uAE30\uB85D\uC758 \uC571 \uB610\uB294 \uD50C\uB7AB\uD3FC\uC774 \uD604\uC7AC \uC694\uCCAD\uACFC \uB2E4\uB985\uB2C8\uB2E4.",
1800
+ ciProviderMismatch: "\uC120\uD0DD\uD55C --ci\uC640 \uC800\uC7A5\uB41C CI \uC81C\uACF5\uC790\uAC00 \uB2E4\uB985\uB2C8\uB2E4. CI \uC124\uC815\uC744 \uD655\uC778\uD558\uC138\uC694.",
1567
1801
  // CI provider 설정 프롬프트
1568
1802
  githubSetupTitle: "GitHub Actions \uC124\uC815",
1569
1803
  gitlabSetupTitle: "GitLab CI \uC124\uC815",
@@ -1599,7 +1833,7 @@ var M3 = catalog(
1599
1833
  noQueueItem: " \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.",
1600
1834
  queueItem: (id) => ` Queue item: ${id}`,
1601
1835
  waitingForBuildNumber: " \uBE4C\uB4DC \uBC88\uD638 \uB300\uAE30 \uC911...",
1602
- noBuildNumber: " \uBE4C\uB4DC \uBC88\uD638\uB97C \uAC00\uC838\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. --skip-build + --version-code \uB85C \uC7AC\uC2DC\uB3C4 \uAC00\uB2A5.",
1836
+ noBuildNumber: " \uBE4C\uB4DC \uBC88\uD638\uB97C \uD655\uC778\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \uC6F9 \uBC30\uD3EC \uAE30\uB85D\uACFC Jenkins \uC2E4\uD589 \uACB0\uACFC\uB97C \uD655\uC778\uD55C \uB4A4 \uC870\uCE58\uD558\uC138\uC694.",
1603
1837
  buildStarted: (n) => ` \uBE4C\uB4DC #${n} \uC2DC\uC791\uB428. \uC644\uB8CC \uB300\uAE30 \uC911...`,
1604
1838
  buildFailed: (result) => `\uBE4C\uB4DC \uC2E4\uD328: ${result}`,
1605
1839
  jenkinsLink: (url) => ` Jenkins: ${url}`,
@@ -1619,7 +1853,7 @@ var M3 = catalog(
1619
1853
  confirmPrompt: "\uACC4\uC18D \uC9C4\uD589\uD560\uAE4C\uC694? [y/N]: ",
1620
1854
  confirmCancelled: "\uCDE8\uC18C\uB428. (--yes \uB85C \uD655\uC778 \uC0DD\uB7B5 \uAC00\uB2A5)",
1621
1855
  pipelineStarting: "\u{1F4E1} \uC11C\uBC84 \uBC30\uD3EC \uD30C\uC774\uD504\uB77C\uC778 \uC2DC\uC791...",
1622
- done: "\uC644\uB8CC. Play Console\uC5D0\uC11C \uBC30\uD3EC \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uC138\uC694."
1856
+ done: "\uBC30\uD3EC \uD30C\uC774\uD504\uB77C\uC778 \uC644\uB8CC. \uC6F9 \uCF58\uC194\uC5D0\uC11C \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uC138\uC694."
1623
1857
  },
1624
1858
  {
1625
1859
  missingValue: (option) => `Missing value: ${option}`,
@@ -1639,6 +1873,15 @@ var M3 = catalog(
1639
1873
  // Server stream
1640
1874
  serverDeployFailed: (status, body) => `Server deploy failed ${status}: ${body}`,
1641
1875
  noSseStream: "No SSE stream",
1876
+ malformedSse: "Invalid server deploy response. Check the deployment in the web console.",
1877
+ incompleteSse: "The server deploy response ended before completion. Check the deployment in the web console.",
1878
+ deploymentLink: (url) => ` Web deployment record: ${url}`,
1879
+ runId: (id) => ` Deployment ID: ${id}`,
1880
+ prepared: (id) => `Deployment ready. Submit with mimi-seed deploy --resume ${id} --yes`,
1881
+ invalidResume: "--resume cannot be combined with --skip-build, --prepare-only, or --dry-run.",
1882
+ resumeNotReady: "Only a ready deployment can be resumed.",
1883
+ runMismatch: "The saved deployment app or platform differs from this request.",
1884
+ ciProviderMismatch: "Selected --ci differs from the configured CI provider. Check the CI configuration.",
1642
1885
  // CI provider setup prompts
1643
1886
  githubSetupTitle: "GitHub Actions setup",
1644
1887
  gitlabSetupTitle: "GitLab CI setup",
@@ -1674,7 +1917,7 @@ var M3 = catalog(
1674
1917
  noQueueItem: " \u26A0 Could not read the queue item ID. The build may have started anyway.",
1675
1918
  queueItem: (id) => ` Queue item: ${id}`,
1676
1919
  waitingForBuildNumber: " Waiting for the build number...",
1677
- noBuildNumber: " Could not read the build number. Retry with --skip-build + --version-code.",
1920
+ noBuildNumber: " Could not confirm the build number. Check the web deployment record and Jenkins outcome before taking action.",
1678
1921
  buildStarted: (n) => ` Build #${n} started. Waiting for it to finish...`,
1679
1922
  buildFailed: (result) => `Build failed: ${result}`,
1680
1923
  jenkinsLink: (url) => ` Jenkins: ${url}`,
@@ -1694,7 +1937,7 @@ var M3 = catalog(
1694
1937
  confirmPrompt: "Continue? [y/N]: ",
1695
1938
  confirmCancelled: "Cancelled. (use --yes to skip this confirmation)",
1696
1939
  pipelineStarting: "\u{1F4E1} Starting the server deploy pipeline...",
1697
- done: "Done. Check the deploy status in Play Console."
1940
+ done: "Deploy pipeline complete. Check its status in the web console."
1698
1941
  }
1699
1942
  );
1700
1943
  function jenkinsHeaders(cfg) {
@@ -1712,7 +1955,7 @@ async function triggerBuild(cfg, jobName, params) {
1712
1955
  body: qs
1713
1956
  });
1714
1957
  if (!res.ok) {
1715
- throw new Error(M3().jenkinsTriggerFailed(res.status, await res.text()));
1958
+ throw new Error(M5().jenkinsTriggerFailed(res.status, await res.text()));
1716
1959
  }
1717
1960
  const location2 = res.headers.get("Location") ?? "";
1718
1961
  const match = location2.match(/\/queue\/item\/(\d+)\//);
@@ -1728,7 +1971,7 @@ async function getQueueBuildNumber(cfg, queueItemId) {
1728
1971
  async function getBuildStatus(cfg, jobName, buildNumber) {
1729
1972
  const url = `${cfg.url}/${jenkinsJobPath(jobName)}/${buildNumber}/api/json`;
1730
1973
  const res = await fetch(url, { headers: jenkinsHeaders(cfg), redirect: "error", signal: AbortSignal.timeout(3e4) });
1731
- if (!res.ok) throw new Error(M3().buildStatusFailed(res.status));
1974
+ if (!res.ok) throw new Error(M5().buildStatusFailed(res.status));
1732
1975
  const data = await res.json();
1733
1976
  return {
1734
1977
  building: data.building ?? true,
@@ -1749,20 +1992,20 @@ async function pollBuildComplete(cfg, jobName, buildNumber, timeoutMs = 30 * 60
1749
1992
  consecutiveErrors = 0;
1750
1993
  } catch {
1751
1994
  consecutiveErrors++;
1752
- if (consecutiveErrors >= 3) throw new Error(M3().jenkinsConnErrorFatal);
1753
- process.stdout.write(`\r \u26A0 ${M3().jenkinsConnErrorRetry(consecutiveErrors)} `);
1995
+ if (consecutiveErrors >= 3) throw new Error(M5().jenkinsConnErrorFatal);
1996
+ process.stdout.write(`\r \u26A0 ${M5().jenkinsConnErrorRetry(consecutiveErrors)} `);
1754
1997
  continue;
1755
1998
  }
1756
1999
  dots = (dots + 1) % 4;
1757
- process.stdout.write(`\r \u23F3 ${M3().buildRunning}${".".repeat(dots + 1)} `);
2000
+ process.stdout.write(`\r \u23F3 ${M5().buildRunning}${".".repeat(dots + 1)} `);
1758
2001
  if (!status.building) {
1759
2002
  process.stdout.write("\n");
1760
2003
  return status.result ?? "FAILURE";
1761
2004
  }
1762
2005
  }
1763
- throw new Error(M3().buildTimeout);
2006
+ throw new Error(M5().buildTimeout);
1764
2007
  }
1765
- async function streamDeploy(webBase, token, body) {
2008
+ async function streamDeploy(webBase, token, appId, body, linkAlreadyPrinted = false) {
1766
2009
  const res = await fetch(`${webBase}/api/deploy`, {
1767
2010
  method: "POST",
1768
2011
  headers: {
@@ -1773,41 +2016,41 @@ async function streamDeploy(webBase, token, body) {
1773
2016
  });
1774
2017
  if (!res.ok) {
1775
2018
  const text = await res.text().catch(() => "");
1776
- throw new Error(M3().serverDeployFailed(res.status, text.slice(0, 200)));
2019
+ throw new Error(M5().serverDeployFailed(res.status, text.slice(0, 200)));
1777
2020
  }
1778
2021
  const reader = res.body?.getReader();
1779
- if (!reader) throw new Error(M3().noSseStream);
1780
- const decoder = new TextDecoder();
1781
- let buf = "";
1782
- while (true) {
1783
- const { done, value } = await reader.read();
1784
- if (done) break;
1785
- buf += decoder.decode(value, { stream: true });
1786
- const lines = buf.split("\n");
1787
- buf = lines.pop() ?? "";
1788
- for (const line of lines) {
1789
- if (!line.startsWith("data: ")) continue;
1790
- try {
1791
- const event = JSON.parse(line.slice(6));
1792
- const icon = PHASE_ICON[event.phase] ?? "\u25B8";
1793
- const color = event.status === "done" ? kleur.green : event.status === "failed" ? kleur.red : event.status === "skipped" ? kleur.yellow : kleur.dim;
1794
- log(` ${icon} ${color(event.message)}`);
1795
- if (event.phase === "error" || event.phase === "verify" && event.status === "failed" || event.phase === "promote" && event.status === "failed") {
1796
- process.exit(1);
1797
- }
1798
- } catch {
2022
+ if (!reader) throw new Error(M5().noSseStream);
2023
+ let linkPrinted = linkAlreadyPrinted;
2024
+ try {
2025
+ await consumeDeployStream(reader, (event) => {
2026
+ if (event.jobId && !linkPrinted) {
2027
+ const link = new URL(`/apps/${encodeURIComponent(appId)}`, webBase);
2028
+ link.searchParams.set("deployment", event.jobId);
2029
+ log(kleur.dim(M5().deploymentLink(link.toString())));
2030
+ linkPrinted = true;
1799
2031
  }
2032
+ const icon = PHASE_ICON[event.phase] ?? "\u25B8";
2033
+ const color = event.status === "done" ? kleur.green : event.status === "failed" ? kleur.red : event.status === "skipped" ? kleur.yellow : kleur.dim;
2034
+ log(` ${icon} ${color(event.message)}`);
2035
+ });
2036
+ } catch (error) {
2037
+ if (error instanceof DeployStreamError) {
2038
+ if (error.reason === "malformed") throw new Error(M5().malformedSse, { cause: error });
2039
+ if (error.reason === "incomplete") throw new Error(M5().incompleteSse, { cause: error });
1800
2040
  }
2041
+ throw error;
1801
2042
  }
1802
2043
  }
1803
2044
  var ANDROID_VERSION_CODE_MAX = 21e8;
1804
2045
  function parseArgs(argv) {
1805
2046
  const args = {
1806
2047
  platform: "android",
2048
+ platformExplicit: false,
1807
2049
  language: "ko-KR",
1808
2050
  dryRun: false,
1809
2051
  yes: false,
1810
2052
  skipBuild: false,
2053
+ prepareOnly: false,
1811
2054
  setupJenkins: false,
1812
2055
  setupGithub: false,
1813
2056
  setupGitlab: false,
@@ -1818,13 +2061,14 @@ function parseArgs(argv) {
1818
2061
  const option = argv[i];
1819
2062
  const value = () => {
1820
2063
  const next = argv[++i];
1821
- if (!next?.trim() || next.startsWith("--")) throw new Error(M3().missingValue(option));
2064
+ if (!next?.trim() || next.startsWith("--")) throw new Error(M5().missingValue(option));
1822
2065
  return next;
1823
2066
  };
1824
2067
  switch (option) {
1825
2068
  case "--platform":
1826
2069
  case "-p":
1827
2070
  args.platform = value();
2071
+ args.platformExplicit = true;
1828
2072
  break;
1829
2073
  case "--app":
1830
2074
  args.appId = value();
@@ -1851,6 +2095,12 @@ function parseArgs(argv) {
1851
2095
  case "--skip-build":
1852
2096
  args.skipBuild = true;
1853
2097
  break;
2098
+ case "--prepare-only":
2099
+ args.prepareOnly = true;
2100
+ break;
2101
+ case "--resume":
2102
+ args.resume = value();
2103
+ break;
1854
2104
  case "--ci":
1855
2105
  args.ci = value();
1856
2106
  break;
@@ -1870,23 +2120,24 @@ function parseArgs(argv) {
1870
2120
  args.setupGitlab = true;
1871
2121
  break;
1872
2122
  default:
1873
- throw new Error(M3().unknownOption(option));
2123
+ throw new Error(M5().unknownOption(option));
1874
2124
  }
1875
2125
  }
1876
- if (!["android", "ios"].includes(args.platform)) throw new Error(M3().invalidPlatform);
1877
- if (!["auto", "jenkins", "github", "gitlab"].includes(args.ci)) throw new Error(M3().invalidCi);
2126
+ if (!["android", "ios"].includes(args.platform)) throw new Error(M5().invalidPlatform);
2127
+ if (!["auto", "jenkins", "github", "gitlab"].includes(args.ci)) throw new Error(M5().invalidCi);
1878
2128
  if (args.versionCode !== void 0 && (!Number.isSafeInteger(args.versionCode) || args.versionCode < 1 || args.versionCode > ANDROID_VERSION_CODE_MAX)) {
1879
- throw new Error(M3().invalidVersion);
2129
+ throw new Error(M5().invalidVersion);
1880
2130
  }
1881
- if ([args.setupJenkins, args.setupGithub, args.setupGitlab].filter(Boolean).length > 1) throw new Error(M3().multipleSetup);
1882
- if (args.dryRun && (args.setupJenkins || args.setupGithub || args.setupGitlab)) throw new Error(M3().drySetup);
2131
+ if ([args.setupJenkins, args.setupGithub, args.setupGitlab].filter(Boolean).length > 1) throw new Error(M5().multipleSetup);
2132
+ if (args.dryRun && (args.setupJenkins || args.setupGithub || args.setupGitlab)) throw new Error(M5().drySetup);
2133
+ if (args.resume && (args.skipBuild || args.prepareOnly || args.dryRun)) throw new Error(M5().invalidResume);
1883
2134
  return args;
1884
2135
  }
1885
2136
  async function promptGitProviderSetup(provider) {
1886
2137
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1887
2138
  const ask2 = (q) => new Promise((resolve) => rl.question(q, (a) => resolve(a.trim())));
1888
2139
  const isGh = provider === "github";
1889
- const m = M3();
2140
+ const m = M5();
1890
2141
  log(kleur.bold(isGh ? m.githubSetupTitle : m.gitlabSetupTitle));
1891
2142
  const tokenLabel = isGh ? m.githubTokenPrompt : m.gitlabTokenPrompt;
1892
2143
  const token = await ask2(tokenLabel);
@@ -1907,36 +2158,36 @@ function resolveCi(ciOption, jenkins, ciProvider) {
1907
2158
  if (ciOption !== "auto") return ciOption;
1908
2159
  if (jenkins?.url && jenkins.token) return "jenkins";
1909
2160
  if (ciProvider) return ciProvider.provider;
1910
- throw new Error(M3().noCiConfig);
2161
+ throw new Error(M5().noCiConfig);
1911
2162
  }
1912
- async function runGitProviderBuild(cfg, args) {
2163
+ async function runGitProviderBuild(cfg, args, appId) {
1913
2164
  let runUrl;
1914
2165
  let runId;
1915
2166
  if (cfg.provider === "github") {
1916
2167
  if (!args.workflow) {
1917
- throw new Error(M3().workflowRequired);
2168
+ throw new Error(M5().workflowRequired);
1918
2169
  }
1919
- log(M3().ghTrigger(kleur.cyan(args.workflow), args.ref));
2170
+ log(M5().ghTrigger(kleur.cyan(args.workflow), args.ref));
1920
2171
  const inputs = {};
1921
- if (args.appId) inputs.MIMI_APP_ID = args.appId;
2172
+ inputs.MIMI_APP_ID = appId;
1922
2173
  inputs.PLATFORM = args.platform;
1923
2174
  const result2 = await ghTriggerWorkflow(cfg, args.workflow, args.ref, inputs);
1924
2175
  if (!result2) {
1925
- throw new Error(M3().ghRunIdFailed);
2176
+ throw new Error(M5().ghRunIdFailed);
1926
2177
  }
1927
2178
  runId = result2.runId;
1928
2179
  runUrl = result2.url;
1929
- log(kleur.dim(M3().ghRunId(runId, runUrl)));
2180
+ log(kleur.dim(M5().ghRunId(runId, runUrl)));
1930
2181
  } else {
1931
- log(M3().glTrigger(args.ref));
2182
+ log(M5().glTrigger(args.ref));
1932
2183
  const variables = { PLATFORM: args.platform };
1933
- if (args.appId) variables.MIMI_APP_ID = args.appId;
2184
+ variables.MIMI_APP_ID = appId;
1934
2185
  const result2 = await glTriggerPipeline(cfg, args.ref, variables);
1935
2186
  runId = result2.pipelineId;
1936
2187
  runUrl = result2.url;
1937
- log(kleur.dim(M3().glPipelineId(runId, runUrl)));
2188
+ log(kleur.dim(M5().glPipelineId(runId, runUrl)));
1938
2189
  }
1939
- log(M3().waitingForCompletion);
2190
+ log(M5().waitingForCompletion);
1940
2191
  let dots = 0;
1941
2192
  const onTick = (status) => {
1942
2193
  dots = (dots + 1) % 4;
@@ -1944,23 +2195,21 @@ async function runGitProviderBuild(cfg, args) {
1944
2195
  };
1945
2196
  const result = cfg.provider === "github" ? await ghPollRun(cfg, runId, onTick) : await glPollPipeline(cfg, runId, onTick);
1946
2197
  process.stdout.write("\n");
1947
- if (result === "success") {
1948
- log(kleur.green(M3().buildSucceeded(runId)));
1949
- return runId;
2198
+ if (result === "success") log(kleur.green(M5().buildSucceeded(runId)));
2199
+ else {
2200
+ log(kleur.red(M5().buildEnded(result)));
2201
+ log(kleur.dim(` ${runUrl}`));
1950
2202
  }
1951
- log(kleur.red(M3().buildEnded(result)));
1952
- log(kleur.dim(` ${runUrl}`));
1953
- log(kleur.dim(M3().alreadyBuiltHint("<N>", args.platform)));
1954
- process.exit(1);
2203
+ return { runId, result };
1955
2204
  }
1956
2205
  async function cmdDeploy(argv) {
1957
2206
  const args = parseArgs(argv);
1958
2207
  if (args.dryRun) {
1959
2208
  const jenkins = loadJenkinsConfig() ?? void 0;
1960
2209
  const ci = args.skipBuild ? null : resolveCi(args.ci, jenkins, loadCiProviderConfig());
1961
- if (ci === "jenkins" && !jenkins) throw new Error(M3().noJenkinsConfig);
2210
+ if (ci === "jenkins" && !jenkins) throw new Error(M5().noJenkinsConfig);
1962
2211
  const plan = ci === "jenkins" && jenkins ? { ...resolveProjectJenkins(jenkins, args.platform), parameters: jenkinsBuildParameters(args.platform, args.ref, args.appId) } : void 0;
1963
- log(M3().dryRunNotice);
2212
+ log(M5().dryRunNotice);
1964
2213
  log(JSON.stringify({
1965
2214
  platform: args.platform,
1966
2215
  appId: args.appId ?? null,
@@ -1974,7 +2223,7 @@ async function cmdDeploy(argv) {
1974
2223
  }
1975
2224
  const cfg = await getEffectiveConfig();
1976
2225
  if (!cfg) {
1977
- log(kleur.red(M3().noAccount));
2226
+ log(kleur.red(M5().noAccount));
1978
2227
  process.exit(1);
1979
2228
  }
1980
2229
  if (args.setupJenkins) {
@@ -1985,47 +2234,104 @@ async function cmdDeploy(argv) {
1985
2234
  if (args.setupGithub) {
1986
2235
  const ciCfg = await promptGitProviderSetup("github");
1987
2236
  saveCiProviderConfig(ciCfg);
1988
- log(kleur.green(M3().githubSaved));
2237
+ log(kleur.green(M5().githubSaved));
1989
2238
  return;
1990
2239
  }
1991
2240
  if (args.setupGitlab) {
1992
2241
  const ciCfg = await promptGitProviderSetup("gitlab");
1993
2242
  saveCiProviderConfig(ciCfg);
1994
- log(kleur.green(M3().gitlabSaved));
2243
+ log(kleur.green(M5().gitlabSaved));
1995
2244
  return;
1996
2245
  }
1997
- if (!args.appId) throw new Error(M3().noAppId);
1998
- if (!args.yes) throw new Error(M3().explicitApproval);
1999
- if (args.skipBuild && !args.versionCode) throw new Error(M3().versionCodeUnknown);
2000
- log(kleur.bold(M3().title(args.platform)));
2001
- if (args.dryRun) log(kleur.yellow(M3().dryRunNotice));
2246
+ const resumed = args.resume ? await getDeployRun(cfg.webBase, cfg.token, args.resume) : null;
2247
+ const projectLink = await findProjectLink(process.cwd());
2248
+ const linkedAppId = resolveLinkedAppId(projectLink, cfg.webBase, args.appId);
2249
+ if (projectLink) validateProjectIdentity(projectLink, await detectHints(process.cwd()));
2250
+ const appId = linkedAppId ?? resumed?.appId;
2251
+ if (!appId) throw new Error(M5().noAppId);
2252
+ if (!args.yes) throw new Error(M5().explicitApproval);
2253
+ if (resumed) {
2254
+ if (resumed.status !== "ready") throw new Error(M5().resumeNotReady);
2255
+ if (resumed.jobId !== args.resume || resumed.appId !== appId || args.platformExplicit && resumed.platform !== args.platform || args.versionCode && args.versionCode !== resumed.versionCode) throw new Error(M5().runMismatch);
2256
+ args.platform = resumed.platform;
2257
+ if (!resumed.versionCode) throw new Error(M5().versionCodeUnknown);
2258
+ } else if (!args.versionCode) throw new Error(M5().versionCodeUnknown);
2259
+ log(kleur.bold(M5().title(args.platform)));
2260
+ if (args.dryRun) log(kleur.yellow(M5().dryRunNotice));
2002
2261
  log("");
2003
- const versionCode = args.versionCode;
2262
+ const versionCode = args.versionCode ?? resumed?.versionCode ?? void 0;
2004
2263
  let deployBuildNumber;
2005
- if (!args.skipBuild) {
2006
- const ciProvider = loadCiProviderConfig();
2264
+ let ciProvider = null;
2265
+ let jenkinsCfg;
2266
+ let jenkinsJobName;
2267
+ let jenkinsParams;
2268
+ let kind = null;
2269
+ if (!resumed && !args.skipBuild) {
2007
2270
  migrateLegacyJenkins();
2008
- const jenkinsCfg = loadJenkinsConfig() ?? void 0;
2009
- const kind = resolveCi(args.ci, jenkinsCfg, ciProvider);
2010
- log(kleur.dim(M3().ciLine(kind)));
2271
+ ciProvider = loadCiProviderConfig();
2272
+ jenkinsCfg = loadJenkinsConfig() ?? void 0;
2273
+ kind = resolveCi(args.ci, jenkinsCfg, ciProvider);
2274
+ if (!["jenkins", "github", "gitlab"].includes(kind)) throw new Error(M5().invalidCi);
2275
+ if (kind === "jenkins") {
2276
+ if (!jenkinsCfg?.url || !jenkinsCfg?.token) throw new Error(M5().noJenkinsConfig);
2277
+ jenkinsJobName = resolveProjectJenkins(jenkinsCfg, args.platform).job;
2278
+ jenkinsParams = jenkinsBuildParameters(args.platform, args.ref, appId);
2279
+ } else if (!ciProvider) {
2280
+ throw new Error(M5().noProviderConfig(kind));
2281
+ } else if (ciProvider.provider !== kind) {
2282
+ throw new Error(M5().ciProviderMismatch);
2283
+ } else if (kind === "github" && !args.workflow) {
2284
+ throw new Error(M5().workflowRequired);
2285
+ }
2286
+ }
2287
+ const runId = resumed?.jobId ?? `run_${randomUUID()}`;
2288
+ const runPayload = { runId, appId, platform: args.platform, fromRef: args.fromRef, toRef: args.toRef };
2289
+ const failRun = async (reason) => {
2290
+ await updateDeployRun(cfg.webBase, cfg.token, { action: "failed", ...runPayload, reason });
2291
+ };
2292
+ if (!resumed) {
2293
+ const started = await updateDeployRun(cfg.webBase, cfg.token, {
2294
+ action: "start",
2295
+ ...runPayload,
2296
+ ...versionCode ? { versionCode } : {}
2297
+ });
2298
+ if (started.jobId !== runId || started.appId !== appId || started.platform !== args.platform) throw new Error(M5().runMismatch);
2299
+ }
2300
+ log(kleur.dim(M5().runId(runId)));
2301
+ const recordLink = new URL(`/apps/${encodeURIComponent(appId)}`, cfg.webBase);
2302
+ recordLink.searchParams.set("deployment", runId);
2303
+ log(kleur.dim(M5().deploymentLink(recordLink.toString())));
2304
+ if (resumed) {
2305
+ await streamDeploy(cfg.webBase, cfg.token, appId, {
2306
+ jobId: runId,
2307
+ appId,
2308
+ platform: args.platform,
2309
+ versionCode,
2310
+ ...resumed.jenkinsBuildNumber ? { buildNumber: resumed.jenkinsBuildNumber } : {},
2311
+ fromRef: resumed.fromRef ?? void 0,
2312
+ toRef: resumed.toRef ?? void 0,
2313
+ language: args.language,
2314
+ dryRun: false
2315
+ }, true);
2316
+ log(kleur.bold(M5().done));
2317
+ return;
2318
+ }
2319
+ if (!args.skipBuild) {
2320
+ await updateDeployRun(cfg.webBase, cfg.token, { action: "building", ...runPayload });
2321
+ log(kleur.dim(M5().ciLine(kind)));
2011
2322
  if (kind === "jenkins") {
2012
- if (!jenkinsCfg?.url || !jenkinsCfg?.token) {
2013
- log(kleur.yellow(M3().noJenkinsConfig));
2014
- process.exit(1);
2015
- }
2016
2323
  const jenkins = jenkinsCfg;
2017
- const { job: jobName } = resolveProjectJenkins(jenkins, args.platform);
2018
- log(M3().jenkinsTrigger(kleur.cyan(jobName)));
2019
- const buildParams = jenkinsBuildParameters(args.platform, args.ref, args.appId);
2020
- const queueItemId = await triggerBuild(jenkins, jobName, buildParams);
2324
+ const jobName = jenkinsJobName;
2325
+ log(M5().jenkinsTrigger(kleur.cyan(jobName)));
2326
+ const queueItemId = await triggerBuild(jenkins, jobName, jenkinsParams);
2021
2327
  if (!queueItemId) {
2022
- log(kleur.yellow(M3().noQueueItem));
2328
+ log(kleur.yellow(M5().noQueueItem));
2023
2329
  } else {
2024
- log(kleur.dim(M3().queueItem(queueItemId)));
2330
+ log(kleur.dim(M5().queueItem(queueItemId)));
2025
2331
  }
2026
2332
  let buildNumber = null;
2027
2333
  if (queueItemId) {
2028
- log(M3().waitingForBuildNumber);
2334
+ log(M5().waitingForBuildNumber);
2029
2335
  for (let i = 0; i < 6; i++) {
2030
2336
  await new Promise((r) => setTimeout(r, 5e3));
2031
2337
  buildNumber = await getQueueBuildNumber(jenkins, queueItemId).catch(() => null);
@@ -2033,46 +2339,61 @@ async function cmdDeploy(argv) {
2033
2339
  }
2034
2340
  }
2035
2341
  if (!buildNumber) {
2036
- log(kleur.yellow(M3().noBuildNumber));
2037
- process.exit(1);
2342
+ log(kleur.yellow(M5().noBuildNumber));
2343
+ throw new Error(M5().noBuildNumber);
2038
2344
  }
2039
- log(M3().buildStarted(buildNumber));
2345
+ log(M5().buildStarted(buildNumber));
2346
+ await updateDeployRun(cfg.webBase, cfg.token, { action: "building", ...runPayload, buildNumber });
2040
2347
  const result = await pollBuildComplete(jenkins, jobName, buildNumber);
2041
2348
  if (result !== "SUCCESS") {
2042
- log(kleur.red(M3().buildFailed(result)));
2043
- log(kleur.dim(M3().jenkinsLink(`${jenkins.url}/job/${encodeURIComponent(jobName)}/${buildNumber}/`)));
2044
- log(kleur.dim(M3().alreadyBuiltHint("<N>", args.platform)));
2045
- process.exit(1);
2349
+ log(kleur.red(M5().buildFailed(result)));
2350
+ log(kleur.dim(M5().jenkinsLink(`${jenkins.url}/job/${encodeURIComponent(jobName)}/${buildNumber}/`)));
2351
+ log(kleur.dim(M5().alreadyBuiltHint("<N>", args.platform)));
2352
+ await failRun("build_failed");
2353
+ throw new Error(M5().buildFailed(result));
2046
2354
  }
2047
- log(kleur.green(M3().buildSucceeded(buildNumber)));
2355
+ log(kleur.green(M5().buildSucceeded(buildNumber)));
2048
2356
  deployBuildNumber = buildNumber;
2049
2357
  if (!versionCode) {
2050
- throw new Error(M3().versionCodeUnsuitable("Jenkins", buildNumber) + "\n" + M3().recommendationNext);
2358
+ await failRun("metadata_missing");
2359
+ throw new Error(M5().versionCodeUnsuitable("Jenkins", buildNumber) + "\n" + M5().recommendationNext);
2051
2360
  }
2052
2361
  } else {
2053
- if (!ciProvider) {
2054
- log(kleur.red(M3().noProviderConfig(kind)));
2055
- process.exit(1);
2362
+ const build = await runGitProviderBuild(ciProvider, args, appId);
2363
+ if (build.result !== "success") {
2364
+ if (build.result === "failure" || build.result === "cancelled") await failRun("build_failed");
2365
+ throw new Error(M5().buildEnded(build.result));
2056
2366
  }
2057
- const buildId = await runGitProviderBuild(ciProvider, args);
2058
2367
  if (!versionCode) {
2059
- log(kleur.red(M3().versionCodeUnsuitable(kind, buildId)));
2060
- log(kleur.dim(M3().recommendation));
2061
- log(kleur.dim(M3().recommendationCi));
2062
- log(kleur.dim(M3().recommendationNext));
2063
- process.exit(1);
2368
+ log(kleur.red(M5().versionCodeUnsuitable(kind, build.runId)));
2369
+ log(kleur.dim(M5().recommendation));
2370
+ log(kleur.dim(M5().recommendationCi));
2371
+ log(kleur.dim(M5().recommendationNext));
2372
+ await failRun("metadata_missing");
2373
+ throw new Error(M5().versionCodeUnknown);
2064
2374
  }
2065
2375
  }
2066
2376
  }
2067
2377
  if (!versionCode) {
2068
- log(kleur.red(M3().versionCodeUnknown));
2069
- process.exit(1);
2378
+ log(kleur.red(M5().versionCodeUnknown));
2379
+ await failRun("metadata_missing");
2380
+ throw new Error(M5().versionCodeUnknown);
2381
+ }
2382
+ await updateDeployRun(cfg.webBase, cfg.token, {
2383
+ action: "ready",
2384
+ ...runPayload,
2385
+ versionCode,
2386
+ ...deployBuildNumber ? { buildNumber: deployBuildNumber } : {}
2387
+ });
2388
+ if (args.prepareOnly) {
2389
+ log(kleur.green(M5().prepared(runId)));
2390
+ return;
2070
2391
  }
2071
- const appId = args.appId;
2072
2392
  log("");
2073
- log(M3().pipelineStarting);
2393
+ log(M5().pipelineStarting);
2074
2394
  log("");
2075
- await streamDeploy(cfg.webBase, cfg.token, {
2395
+ await streamDeploy(cfg.webBase, cfg.token, appId, {
2396
+ jobId: runId,
2076
2397
  appId,
2077
2398
  platform: args.platform,
2078
2399
  versionCode,
@@ -2081,24 +2402,23 @@ async function cmdDeploy(argv) {
2081
2402
  toRef: args.toRef,
2082
2403
  language: args.language,
2083
2404
  dryRun: args.dryRun
2084
- });
2405
+ }, true);
2085
2406
  log("");
2086
- log(kleur.bold(M3().done));
2087
- log(kleur.dim(" https://play.google.com/console/developers"));
2407
+ log(kleur.bold(M5().done));
2088
2408
  }
2089
2409
 
2090
2410
  // src/telemetry.ts
2091
- import fs9 from "fs";
2411
+ import fs10 from "fs";
2092
2412
  import os6 from "os";
2093
- import path10 from "path";
2094
- import { createHmac, randomUUID } from "crypto";
2413
+ import path11 from "path";
2414
+ import { createHmac, randomUUID as randomUUID2 } from "crypto";
2095
2415
 
2096
2416
  // package.json
2097
- var version = "0.19.17";
2417
+ var version = "0.19.18";
2098
2418
 
2099
2419
  // src/telemetry.ts
2100
2420
  var ENDPOINT = "https://mimi-seed.pryzm.gg/api/sdk-usage";
2101
- var M4 = catalog(
2421
+ var M6 = catalog(
2102
2422
  {
2103
2423
  notice: "\uC120\uD0DD\uC801 \uC0AC\uC6A9\uB7C9 \uCE21\uC815\uC740 \uAEBC\uC838 \uC788\uC2B5\uB2C8\uB2E4. \uC548\uB0B4: mimi-seed telemetry --help\n",
2104
2424
  enabled: "\uC0AC\uC6A9\uB7C9 \uCE21\uC815 \uCF1C\uC9D0. \uC784\uC758 \uC124\uCE58 ID\xB7\uD504\uB85C\uC81D\uD2B8 \uD574\uC2DC\xB7\uBC84\uC804\xB7OS\xB7\uACB0\uACFC \uCF54\uB4DC\xB7\uC2E4\uD589\uC2DC\uAC04\uC744 \uC804\uC1A1\uD569\uB2C8\uB2E4. \uB044\uAE30: mimi-seed telemetry off\n",
@@ -2112,46 +2432,46 @@ var M4 = catalog(
2112
2432
  invalid: "Usage: mimi-seed telemetry on|off|status\n"
2113
2433
  }
2114
2434
  );
2115
- var location = () => path10.join(os6.homedir(), ".mimi-seed", "telemetry.json");
2435
+ var location = () => path11.join(os6.homedir(), ".mimi-seed", "telemetry.json");
2116
2436
  function readConsent() {
2117
2437
  try {
2118
- return JSON.parse(fs9.readFileSync(location(), "utf8"));
2438
+ return JSON.parse(fs10.readFileSync(location(), "utf8"));
2119
2439
  } catch {
2120
2440
  return { enabled: false };
2121
2441
  }
2122
2442
  }
2123
2443
  function writeConsent(value) {
2124
2444
  const file = location();
2125
- fs9.mkdirSync(path10.dirname(file), { recursive: true, mode: 448 });
2126
- const temp = `${file}.${randomUUID()}.tmp`;
2127
- fs9.writeFileSync(temp, JSON.stringify(value), { mode: 384 });
2128
- fs9.renameSync(temp, file);
2445
+ fs10.mkdirSync(path11.dirname(file), { recursive: true, mode: 448 });
2446
+ const temp = `${file}.${randomUUID2()}.tmp`;
2447
+ fs10.writeFileSync(temp, JSON.stringify(value), { mode: 384 });
2448
+ fs10.renameSync(temp, file);
2129
2449
  }
2130
2450
  function telemetryEnabled() {
2131
2451
  if (process.env.MIMI_SEED_TELEMETRY !== void 0) return process.env.MIMI_SEED_TELEMETRY === "1";
2132
2452
  return readConsent().enabled === true;
2133
2453
  }
2134
2454
  function telemetryNotice() {
2135
- return telemetryEnabled() ? "" : M4().notice;
2455
+ return telemetryEnabled() ? "" : M6().notice;
2136
2456
  }
2137
2457
  function cmdTelemetry(argv) {
2138
2458
  const action = argv[0] ?? "status";
2139
2459
  if (argv.length > 1 || !["on", "off", "status"].includes(action)) {
2140
- process.stderr.write(M4().invalid);
2460
+ process.stderr.write(M6().invalid);
2141
2461
  process.exitCode = 2;
2142
2462
  return;
2143
2463
  }
2144
2464
  if (action === "on") {
2145
2465
  const consent = readConsent();
2146
- writeConsent({ enabled: true, installationId: consent.installationId ?? randomUUID(), salt: consent.salt ?? randomUUID() });
2466
+ writeConsent({ enabled: true, installationId: consent.installationId ?? randomUUID2(), salt: consent.salt ?? randomUUID2() });
2147
2467
  }
2148
2468
  if (action === "off") writeConsent({ enabled: false });
2149
- process.stdout.write(telemetryEnabled() ? M4().enabled : M4().disabled);
2469
+ process.stdout.write(telemetryEnabled() ? M6().enabled : M6().disabled);
2150
2470
  }
2151
2471
  function frameworkAt(root) {
2152
- if (fs9.existsSync(path10.join(root, "ProjectSettings", "ProjectSettings.asset"))) return "unity";
2472
+ if (fs10.existsSync(path11.join(root, "ProjectSettings", "ProjectSettings.asset"))) return "unity";
2153
2473
  try {
2154
- const pkg = JSON.parse(fs9.readFileSync(path10.join(root, "package.json"), "utf8"));
2474
+ const pkg = JSON.parse(fs10.readFileSync(path11.join(root, "package.json"), "utf8"));
2155
2475
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
2156
2476
  if (deps.expo) return "expo";
2157
2477
  if (deps["react-native"]) return "react-native";
@@ -2164,20 +2484,20 @@ function usageRun(command, projectPath) {
2164
2484
  };
2165
2485
  try {
2166
2486
  const consent = readConsent();
2167
- const installationId = consent.installationId ?? randomUUID();
2168
- const salt = consent.salt ?? randomUUID();
2487
+ const installationId = consent.installationId ?? randomUUID2();
2488
+ const salt = consent.salt ?? randomUUID2();
2169
2489
  if (!consent.installationId || !consent.salt) writeConsent({ enabled: consent.enabled, installationId, salt });
2170
- let normalized = path10.resolve(projectPath);
2490
+ let normalized = path11.resolve(projectPath);
2171
2491
  try {
2172
- normalized = fs9.realpathSync(normalized);
2492
+ normalized = fs10.realpathSync(normalized);
2173
2493
  } catch {
2174
2494
  }
2175
2495
  if (process.platform === "win32") normalized = normalized.toLowerCase();
2176
- const base = {
2496
+ const base2 = {
2177
2497
  schema: 1,
2178
2498
  installationId,
2179
2499
  projectId: createHmac("sha256", salt).update(normalized).digest("hex"),
2180
- runId: randomUUID(),
2500
+ runId: randomUUID2(),
2181
2501
  version,
2182
2502
  os: process.platform,
2183
2503
  ci: Boolean(process.env.CI),
@@ -2188,7 +2508,7 @@ function usageRun(command, projectPath) {
2188
2508
  return async (outcome, report) => {
2189
2509
  if (!telemetryEnabled()) return;
2190
2510
  const payload = {
2191
- ...base,
2511
+ ...base2,
2192
2512
  outcome,
2193
2513
  durationMs: Math.min(864e5, Math.max(0, Date.now() - started)),
2194
2514
  platforms: report?.platforms ?? [],
@@ -2475,6 +2795,7 @@ export {
2475
2795
  usageRun,
2476
2796
  MCP_PKG,
2477
2797
  runMcpBin,
2798
+ linkProject,
2478
2799
  cmdDeploy,
2479
2800
  parseSetupArgs,
2480
2801
  resolveMode,
package/dist/index.js CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  hasAnyProjectSignal,
18
18
  isLang,
19
19
  isSatisfied,
20
+ linkProject,
20
21
  manifestServiceEntries,
21
22
  migrateLegacyJenkins,
22
23
  resolveLang,
@@ -27,7 +28,7 @@ import {
27
28
  usageRun,
28
29
  writeConfig,
29
30
  writeSettings
30
- } from "./chunk-W62LVTR7.js";
31
+ } from "./chunk-L3OYPC6L.js";
31
32
 
32
33
  // src/index.ts
33
34
  import os3 from "os";
@@ -2169,7 +2170,7 @@ async function cmdAuth(args) {
2169
2170
  if (sub === "threads") return void exitWith(await runMcpBin("mimi-seed-social-auth", ["threads", ...rest]));
2170
2171
  if (sub === "tiktok") return void exitWith(await runMcpBin("mimi-seed-tiktok-business-auth", rest));
2171
2172
  if (sub === "ci") {
2172
- const { cmdSetup: cmdSetup2 } = await import("./setup-RUQ7EBK6.js");
2173
+ const { cmdSetup: cmdSetup2 } = await import("./setup-ICXOGALT.js");
2173
2174
  await cmdSetup2(["--only", "github,gitlab", "--reconnect", "github,gitlab"]);
2174
2175
  return;
2175
2176
  }
@@ -2744,6 +2745,8 @@ ${kleur9.dim("setup \uB9C8\uBC95\uC0AC\uAC00 \uCCAB \uC2E4\uD589 \uB54C \uBB3C\u
2744
2745
  --dry-run \uB85C\uCEEC \uACC4\uD68D\uB9CC \uCD9C\uB825 (\uB124\uD2B8\uC6CC\uD06C\xB7\uBE4C\uB4DC\xB7\uC4F0\uAE30 \uC5C6\uC74C)
2745
2746
  --yes, -y CI \uC2E4\uD589\xB7\uC2A4\uD1A0\uC5B4 \uBC30\uD3EC \uBA85\uC2DC\uC801 \uC2B9\uC778 (\uC2E4\uD589 \uD544\uC218)
2746
2747
  --skip-build CI \uBE4C\uB4DC \uAC74\uB108\uB700 (--version-code \uD544\uC218)
2748
+ --prepare-only \uBE4C\uB4DC\xB7\uBC84\uC804 \uD655\uC778 \uD6C4 \uC6F9 \uAE30\uB85D\uC744 \uC900\uBE44 \uC0C1\uD0DC\uB85C \uB450\uACE0 \uC81C\uCD9C \uC911\uB2E8
2749
+ --resume <runId> \uC900\uBE44\uB41C \uBC30\uD3EC\uB97C CI \uBE4C\uB4DC \uC5C6\uC774 \uC774\uC5B4\uC11C \uC81C\uCD9C
2747
2750
  --ci jenkins|github|gitlab CI \uAC15\uC81C \uC120\uD0DD (\uAE30\uBCF8: auto)
2748
2751
  --workflow <file> GitHub workflow \uD30C\uC77C (\uC608: deploy.yml)
2749
2752
  --ref <branch|tag> CI \uC18C\uC2A4 (\uAE30\uBCF8: main, Jenkins\uB294 \uBE0C\uB79C\uCE58\uB9CC)
@@ -2915,6 +2918,8 @@ Options:
2915
2918
  --dry-run print a local plan (no network, builds or writes)
2916
2919
  --yes, -y explicitly approve CI and store release (required)
2917
2920
  --skip-build skip the CI build (--version-code required)
2921
+ --prepare-only stop after build/version verification with a ready web record
2922
+ --resume <runId> submit a ready deployment without rebuilding
2918
2923
  --ci jenkins|github|gitlab force the CI provider (default: auto)
2919
2924
  --workflow <file> GitHub workflow file (e.g. deploy.yml)
2920
2925
  --ref <branch|tag> CI source (default: main; Jenkins: branches only)
@@ -2993,6 +2998,13 @@ async function cmdInit(args) {
2993
2998
  log4(kleur9.red(M9().syncFailed(result.text)));
2994
2999
  } else {
2995
3000
  for (const line of result.text.split("\n")) log4(" " + line);
3001
+ if (hints.length === 1 && (hints[0].packageName || hints[0].bundleId)) {
3002
+ try {
3003
+ await linkProject(cwd, cfg2.webBase, cfg2.token, hints[0]);
3004
+ } catch (error) {
3005
+ log4(kleur9.yellow(String(error)));
3006
+ }
3007
+ }
2996
3008
  }
2997
3009
  }
2998
3010
  const manifest2 = await ensureReleaseManifest(cwd);
@@ -3034,6 +3046,13 @@ async function cmdInit(args) {
3034
3046
  log4(kleur9.red(M9().syncFailed(result.text)));
3035
3047
  } else {
3036
3048
  for (const line of result.text.split("\n")) log4(" " + line);
3049
+ if (hints.length === 1 && (hints[0].packageName || hints[0].bundleId)) {
3050
+ try {
3051
+ await linkProject(cwd, cfg.webBase, cfg.token, hints[0]);
3052
+ } catch (error) {
3053
+ log4(kleur9.yellow(String(error)));
3054
+ }
3055
+ }
3037
3056
  }
3038
3057
  log4("");
3039
3058
  }
@@ -3,7 +3,7 @@ import {
3
3
  cmdSetup,
4
4
  parseSetupArgs,
5
5
  resolveMode
6
- } from "./chunk-W62LVTR7.js";
6
+ } from "./chunk-L3OYPC6L.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.19.17",
4
+ "version": "0.19.18",
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"