mimi-seed 0.19.8 → 0.19.10

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 (3) hide show
  1. package/README.md +2 -1
  2. package/dist/index.js +953 -50
  3. package/package.json +3 -1
package/README.md CHANGED
@@ -11,7 +11,8 @@ npx mimi-seed check --local
11
11
  ```
12
12
 
13
13
  로그인 없이 현재 저장소의 앱 식별자, Android Target API, Google Play Billing 지원 상태를 먼저 검사합니다.
14
- 정책 결과에는 근거 파일과 공식 출처가 포함됩니다. 실제 스토어 상태까지 연결하려면 그다음 실행합니다.
14
+ 검사기는 CLI에 번들되어 전체 MCP 패키지를 추가 설치하지 않으며, 저장소 전용 모드에서는 네트워크도 호출하지
15
+ 않습니다. 정책 결과에는 근거 파일과 공식 출처가 포함됩니다. 실제 스토어 상태까지 연결하려면 그다음 실행합니다.
15
16
 
16
17
  ```bash
17
18
  npx mimi-seed init
package/dist/index.js CHANGED
@@ -33,8 +33,8 @@ import {
33
33
 
34
34
  // src/index.ts
35
35
  import os3 from "os";
36
- import fs4 from "fs";
37
- import path4 from "path";
36
+ import fs6 from "fs";
37
+ import path6 from "path";
38
38
  import kleur9 from "kleur";
39
39
  import open from "open";
40
40
 
@@ -260,9 +260,877 @@ async function cmdDoctor() {
260
260
 
261
261
  // src/check.ts
262
262
  import kleur2 from "kleur";
263
+
264
+ // src/checks/release-doctor-render.ts
265
+ var COPY = {
266
+ ko: {
267
+ title: "Mimi Seed Release Doctor \u2014 \uB85C\uADF8\uC778 \uC5C6\uB294 \uCD9C\uC2DC \uC810\uAC80",
268
+ platforms: "\uAC10\uC9C0 \uD50C\uB7AB\uD3FC",
269
+ summary: (blockers, warnings) => `\uACB0\uACFC: \uBE14\uB85C\uCEE4 ${blockers} \xB7 \uACBD\uACE0 ${warnings}`,
270
+ labels: { blocker: "\uBE14\uB85C\uCEE4", warning: "\uACBD\uACE0", info: "\uD655\uC778" },
271
+ evidence: "\uADFC\uAC70",
272
+ action: "\uC870\uCE58",
273
+ source: "\uCD9C\uCC98",
274
+ connected: "\uC2A4\uD1A0\uC5B4 \uC5F0\uACB0 \uD6C4 \uCD94\uAC00 \uAC80\uC0AC",
275
+ connectedChecks: [
276
+ "\uC2A4\uD1A0\uC5B4 \uB4F1\uB85D\uC815\uBCF4\uC640 \uC2A4\uD06C\uB9B0\uC0F7",
277
+ "\uC5C5\uB85C\uB4DC\uB41C \uBE4C\uB4DC\uC758 \uC874\uC7AC \uC5EC\uBD80\uC640 \uCC98\uB9AC \uC0C1\uD0DC",
278
+ "App Store \uC5F0\uB839\uB4F1\uAE09 \uC751\uB2F5\uACFC Google Play \uC120\uC5B8",
279
+ "\uC2EC\uC0AC \uC81C\uCD9C\uACFC \uB2E8\uACC4\uC801 \uCD9C\uC2DC \uC0C1\uD0DC"
280
+ ],
281
+ ready: "\uB85C\uCEEC \uAC80\uC0AC\uC5D0\uC11C \uC81C\uCD9C \uCC28\uB2E8 \uC694\uC778\uC744 \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
282
+ partial: "\uC774 \uACB0\uACFC\uB294 \uC800\uC7A5\uC18C\uC758 \uACB0\uC815\uC801 \uC99D\uAC70\uB9CC \uAC80\uC0AC\uD569\uB2C8\uB2E4. \uC2A4\uD1A0\uC5B4 \uC0C1\uD0DC\uB294 \uC5F0\uACB0 \uD6C4 \uBCC4\uB3C4\uB85C \uD655\uC778\uD569\uB2C8\uB2E4."
283
+ },
284
+ en: {
285
+ title: "Mimi Seed Release Doctor \u2014 no-login release check",
286
+ platforms: "Platforms",
287
+ summary: (blockers, warnings) => `Result: ${blockers} blockers \xB7 ${warnings} warnings`,
288
+ labels: { blocker: "BLOCKER", warning: "WARNING", info: "INFO" },
289
+ evidence: "Evidence",
290
+ action: "Action",
291
+ source: "Source",
292
+ connected: "Checked after connecting stores",
293
+ connectedChecks: void 0,
294
+ ready: "No submission blocker was found by the local checks.",
295
+ partial: "This report checks deterministic repository evidence only. Store state is checked separately after connection."
296
+ }
297
+ };
298
+ function renderFinding(finding, copy, lang) {
299
+ const localized = lang === "ko" ? finding.ko : void 0;
300
+ const lines = [
301
+ `[${copy.labels[finding.severity]}] ${localized?.title ?? finding.title}`,
302
+ ` ${localized?.detail ?? finding.detail}`
303
+ ];
304
+ if (finding.file) lines.push(` ${copy.evidence}: ${finding.file}`);
305
+ const action = localized?.action ?? finding.action;
306
+ if (action) lines.push(` ${copy.action}: ${action}`);
307
+ if (finding.sourceUrl) lines.push(` ${copy.source}: ${finding.sourceUrl}`);
308
+ return lines;
309
+ }
310
+ function renderReleaseDoctor(report, lang) {
311
+ const copy = COPY[lang];
312
+ const lines = [
313
+ "",
314
+ copy.title,
315
+ report.projectPath,
316
+ `${copy.platforms}: ${report.platforms.length ? report.platforms.join(" + ") : "-"}`,
317
+ copy.summary(report.counts.blocker, report.counts.warning),
318
+ ""
319
+ ];
320
+ for (const finding of report.findings) lines.push(...renderFinding(finding, copy, lang), "");
321
+ if (report.counts.blocker === 0) lines.push(`\u2713 ${copy.ready}`, "");
322
+ const connectedChecks = copy.connectedChecks ?? report.coverage.requiresStoreConnection;
323
+ lines.push(copy.partial, "", `${copy.connected}:`, ...connectedChecks.map((item) => ` - ${item}`), "");
324
+ return lines.join("\n");
325
+ }
326
+
327
+ // src/checks/release-doctor.ts
328
+ import fs2 from "fs/promises";
329
+ import path2 from "path";
330
+
331
+ // src/checks/billing.ts
332
+ import fs from "fs/promises";
333
+ import path from "path";
334
+ var BILLING_MODULE = /com\.android\.billingclient:billing(?:-ktx)?/;
335
+ var LITERAL_DEPENDENCY = /com\.android\.billingclient:billing(?:-ktx)?:([0-9]+(?:\.[0-9A-Za-z_-]+){0,3})/g;
336
+ var VARIABLE_DEPENDENCY = /com\.android\.billingclient:billing(?:-ktx)?:\$\{?([A-Za-z_][A-Za-z0-9_.-]*)\}?/g;
337
+ var VERSION_ASSIGNMENT = /(?:^|\s)([A-Za-z_][A-Za-z0-9_.-]*)\s*(?:=|:)\s*["']([0-9]+(?:\.[0-9A-Za-z_-]+){0,3})["']/gm;
338
+ var BILLING_SUPPORT_SCHEDULE = [
339
+ { major: 5, submissionDeadline: "2024-08-31", extensionDeadline: "2024-11-01" },
340
+ { major: 6, submissionDeadline: "2025-08-31", extensionDeadline: "2025-11-01" },
341
+ { major: 7, submissionDeadline: "2026-08-31", extensionDeadline: "2026-11-01" },
342
+ { major: 8, submissionDeadline: "2027-08-31", extensionDeadline: "2027-11-01" },
343
+ { major: 9, submissionDeadline: "2028-08-31", extensionDeadline: "2028-11-01" }
344
+ ];
345
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
346
+ ".git",
347
+ ".gradle",
348
+ ".idea",
349
+ "build",
350
+ "dist",
351
+ "node_modules",
352
+ "Pods",
353
+ "DerivedData"
354
+ ]);
355
+ async function walk(root, maxDepth = 7) {
356
+ const result = [];
357
+ async function visit(dir, depth) {
358
+ if (depth > maxDepth) return;
359
+ let entries;
360
+ try {
361
+ entries = await fs.readdir(dir, { withFileTypes: true });
362
+ } catch {
363
+ return;
364
+ }
365
+ for (const entry of entries) {
366
+ if (entry.isDirectory()) {
367
+ if (!SKIP_DIRS.has(entry.name)) await visit(path.join(dir, entry.name), depth + 1);
368
+ } else if (entry.isFile() && (entry.name === "build.gradle" || entry.name === "build.gradle.kts" || entry.name === "libs.versions.toml" || entry.name === "package.json")) {
369
+ result.push(path.join(dir, entry.name));
370
+ }
371
+ }
372
+ }
373
+ await visit(root, 0);
374
+ return result;
375
+ }
376
+ function parseCatalog(text) {
377
+ const versions = /* @__PURE__ */ new Map();
378
+ const libraries = /* @__PURE__ */ new Map();
379
+ const bundles = /* @__PURE__ */ new Map();
380
+ let section2 = "";
381
+ for (const rawLine of text.split(/\r?\n/)) {
382
+ const line = rawLine.replace(/\s+#.*$/, "").trim();
383
+ if (!line) continue;
384
+ const sectionMatch = line.match(/^\[([^\]]+)]$/);
385
+ if (sectionMatch) {
386
+ section2 = sectionMatch[1];
387
+ continue;
388
+ }
389
+ if (section2 === "versions") {
390
+ const match2 = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*["']([^"']+)["']/);
391
+ if (match2) versions.set(match2[1], match2[2]);
392
+ continue;
393
+ }
394
+ if (section2 === "bundles") {
395
+ const match2 = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*\[([^\]]*)]/);
396
+ if (match2) {
397
+ bundles.set(match2[1], [...match2[2].matchAll(/["']([^"']+)["']/g)].map((item) => item[1]));
398
+ }
399
+ continue;
400
+ }
401
+ if (section2 !== "libraries") continue;
402
+ const shorthand = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*["']([^"']+)["']/);
403
+ if (shorthand) {
404
+ const coordinates = shorthand[2].split(":");
405
+ libraries.set(shorthand[1], {
406
+ module: coordinates.length >= 2 ? `${coordinates[0]}:${coordinates[1]}` : void 0,
407
+ version: coordinates.length >= 3 ? coordinates.slice(2).join(":") : void 0
408
+ });
409
+ continue;
410
+ }
411
+ const match = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*\{(.+)}\s*$/);
412
+ if (!match) continue;
413
+ const body = match[2];
414
+ const explicitModule = body.match(/module\s*=\s*["']([^"']+)["']/)?.[1];
415
+ const group = body.match(/group\s*=\s*["']([^"']+)["']/)?.[1];
416
+ const name = body.match(/name\s*=\s*["']([^"']+)["']/)?.[1];
417
+ const module = explicitModule ?? (group && name ? `${group}:${name}` : void 0);
418
+ const version = body.match(/(?:^|,)\s*version\s*=\s*["']([^"']+)["']/)?.[1];
419
+ const versionRef = body.match(/version\.ref\s*=\s*["']([^"']+)["']/)?.[1];
420
+ libraries.set(match[1], { module, version, versionRef });
421
+ }
422
+ return { versions, libraries, bundles };
423
+ }
424
+ function collectVariables(text) {
425
+ const result = /* @__PURE__ */ new Map();
426
+ for (const match of text.matchAll(VERSION_ASSIGNMENT)) result.set(match[1], match[2]);
427
+ return result;
428
+ }
429
+ function normalizeAlias(alias) {
430
+ return alias.replace(/^libs\./, "").replace(/\./g, "-");
431
+ }
432
+ function majorOf(version) {
433
+ const major = Number.parseInt(version.split(".")[0], 10);
434
+ return Number.isFinite(major) ? major : null;
435
+ }
436
+ async function nearestNodePackage(packageDir, root) {
437
+ let current = packageDir;
438
+ while (isWithin(root, current)) {
439
+ const candidate = path.join(current, "node_modules", "react-native-iap");
440
+ try {
441
+ if ((await fs.stat(candidate)).isDirectory()) return candidate;
442
+ } catch {
443
+ }
444
+ if (current === root) break;
445
+ current = path.dirname(current);
446
+ }
447
+ return null;
448
+ }
449
+ async function reactNativeIapEvidence(root, manifestFile, manifestText, resolveTransitive) {
450
+ let manifest;
451
+ try {
452
+ manifest = JSON.parse(manifestText);
453
+ } catch {
454
+ return null;
455
+ }
456
+ const dependencyGroups = ["dependencies", "devDependencies", "optionalDependencies"].map((key) => manifest[key]).filter((value) => Boolean(value) && typeof value === "object");
457
+ const declaredVersion = dependencyGroups.map((group) => group["react-native-iap"]).find((value) => typeof value === "string");
458
+ if (!declaredVersion) return null;
459
+ const relativeManifest = path.relative(root, manifestFile).replace(/\\/g, "/");
460
+ const installedDir = await nearestNodePackage(path.dirname(manifestFile), root);
461
+ if (!installedDir) {
462
+ return {
463
+ file: relativeManifest,
464
+ module: "com.android.billingclient:billing",
465
+ expression: `react-native-iap ${declaredVersion} is declared but not installed; transitive Billing version is unresolved`,
466
+ source: "unresolved"
467
+ };
468
+ }
469
+ const directCandidates = [
470
+ path.join(installedDir, "android", "build.gradle"),
471
+ path.join(installedDir, "android", "build.gradle.kts")
472
+ ];
473
+ for (const candidate of directCandidates) {
474
+ try {
475
+ const text = await fs.readFile(candidate, "utf8");
476
+ const direct = [...text.matchAll(LITERAL_DEPENDENCY)][0];
477
+ if (direct) {
478
+ return {
479
+ file: relativeManifest,
480
+ module: direct[0].slice(0, direct[0].lastIndexOf(":")),
481
+ version: direct[1],
482
+ expression: `react-native-iap ${declaredVersion} native dependency`,
483
+ source: "transitive"
484
+ };
485
+ }
486
+ } catch {
487
+ }
488
+ }
489
+ let openIapVersion;
490
+ try {
491
+ const versions = JSON.parse(await fs.readFile(path.join(installedDir, "openiap-versions.json"), "utf8"));
492
+ if (typeof versions.google === "string") openIapVersion = versions.google;
493
+ } catch {
494
+ }
495
+ if (!openIapVersion || !/^[0-9A-Za-z][0-9A-Za-z._-]*$/.test(openIapVersion)) {
496
+ return {
497
+ file: relativeManifest,
498
+ module: "com.android.billingclient:billing",
499
+ expression: `react-native-iap ${declaredVersion} detected; transitive Billing version is unresolved`,
500
+ source: "unresolved"
501
+ };
502
+ }
503
+ const coordinate = `io.github.hyochan.openiap:openiap-google:${openIapVersion}`;
504
+ if (!resolveTransitive) {
505
+ return {
506
+ file: relativeManifest,
507
+ module: "com.android.billingclient:billing",
508
+ expression: `react-native-iap ${declaredVersion} -> ${coordinate}; transitive lookup unavailable in repository-only mode`,
509
+ source: "unresolved"
510
+ };
511
+ }
512
+ try {
513
+ const resolved = await resolveTransitive(openIapVersion);
514
+ if (resolved) {
515
+ return {
516
+ file: relativeManifest,
517
+ module: resolved.module,
518
+ version: resolved.version,
519
+ expression: `react-native-iap ${declaredVersion} -> ${coordinate}`,
520
+ source: "transitive"
521
+ };
522
+ }
523
+ } catch {
524
+ }
525
+ return {
526
+ file: relativeManifest,
527
+ module: "com.android.billingclient:billing",
528
+ expression: `react-native-iap ${declaredVersion} -> ${coordinate}; Maven Billing version lookup failed`,
529
+ source: "unresolved"
530
+ };
531
+ }
532
+ function policyAt(now) {
533
+ const deadlineEnd = (date) => /* @__PURE__ */ new Date(`${date}T23:59:59.999Z`);
534
+ const next = BILLING_SUPPORT_SCHEDULE.find((row) => now <= deadlineEnd(row.submissionDeadline));
535
+ const lastExpired = [...BILLING_SUPPORT_SCHEDULE].filter((row) => now > deadlineEnd(row.submissionDeadline)).at(-1);
536
+ return {
537
+ minimumSupportedMajor: next?.major ?? null,
538
+ submissionDeadline: lastExpired?.submissionDeadline ?? BILLING_SUPPORT_SCHEDULE[0].submissionDeadline,
539
+ extensionDeadline: lastExpired?.extensionDeadline ?? BILLING_SUPPORT_SCHEDULE[0].extensionDeadline,
540
+ latestKnownMajor: BILLING_SUPPORT_SCHEDULE.at(-1).major,
541
+ scheduleCurrent: Boolean(next),
542
+ knownSchedule: BILLING_SUPPORT_SCHEDULE.map((row) => ({ ...row })),
543
+ sourceUrl: "https://developer.android.com/google/play/billing/deprecation-faq"
544
+ };
545
+ }
546
+ function scheduleForMajor(major) {
547
+ return BILLING_SUPPORT_SCHEDULE.find((row) => row.major === major);
548
+ }
549
+ function catalogScope(file) {
550
+ const parent = path.dirname(file);
551
+ return path.basename(parent) === "gradle" ? path.dirname(parent) : parent;
552
+ }
553
+ function isWithin(scope, file) {
554
+ const relative = path.relative(scope, file);
555
+ return relative === "" || !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative);
556
+ }
557
+ async function checkBillingCompliance(projectPath, now = /* @__PURE__ */ new Date(), options = {}) {
558
+ const root = path.resolve(projectPath);
559
+ let rootStat;
560
+ try {
561
+ rootStat = await fs.stat(root);
562
+ } catch {
563
+ throw new Error(`Android project path does not exist or is not readable: ${root}`);
564
+ }
565
+ if (!rootStat.isDirectory()) throw new Error(`Android project path is not a directory: ${root}`);
566
+ const files = await walk(root);
567
+ const texts = /* @__PURE__ */ new Map();
568
+ for (const file of files) texts.set(file, await fs.readFile(file, "utf8"));
569
+ const catalogs = files.filter((file) => path.basename(file) === "libs.versions.toml").map((file) => ({ file, scope: catalogScope(file), info: parseCatalog(texts.get(file) ?? "") }));
570
+ const variablesByFile = /* @__PURE__ */ new Map();
571
+ for (const [file, text] of texts) {
572
+ if (path.basename(file) === "libs.versions.toml") continue;
573
+ variablesByFile.set(file, collectVariables(text));
574
+ }
575
+ const catalogFor = (file) => catalogs.filter((entry) => isWithin(entry.scope, file)).sort((left, right) => right.scope.length - left.scope.length)[0]?.info ?? (catalogs.length === 1 ? catalogs[0].info : void 0);
576
+ const variableFor = (file, key) => [...variablesByFile.entries()].filter(([candidate]) => candidate === file || isWithin(path.dirname(candidate), file)).sort(([left], [right]) => path.dirname(right).length - path.dirname(left).length).map(([, variables]) => variables.get(key)).find((value) => value !== void 0);
577
+ const evidence = [];
578
+ for (const [file, text] of texts) {
579
+ if (path.basename(file) !== "package.json") continue;
580
+ const transitive = await reactNativeIapEvidence(
581
+ root,
582
+ file,
583
+ text,
584
+ options.resolveTransitive
585
+ );
586
+ if (transitive && !evidence.some((row) => row.expression === transitive.expression)) evidence.push(transitive);
587
+ }
588
+ for (const [file, text] of texts) {
589
+ const relative = path.relative(root, file).replace(/\\/g, "/");
590
+ if (path.basename(file) === "libs.versions.toml" || path.basename(file) === "package.json") {
591
+ continue;
592
+ }
593
+ const catalog2 = catalogFor(file);
594
+ for (const match of text.matchAll(LITERAL_DEPENDENCY)) {
595
+ evidence.push({
596
+ file: relative,
597
+ module: match[0].slice(0, match[0].lastIndexOf(":")),
598
+ version: match[1],
599
+ source: "literal"
600
+ });
601
+ }
602
+ for (const match of text.matchAll(VARIABLE_DEPENDENCY)) {
603
+ const version = variableFor(file, match[1]);
604
+ evidence.push({
605
+ file: relative,
606
+ module: match[0].slice(0, match[0].lastIndexOf(":")),
607
+ version,
608
+ expression: match[1],
609
+ source: version ? "variable" : "unresolved"
610
+ });
611
+ }
612
+ for (const bundleMatch of text.matchAll(/\blibs\.bundles\.([A-Za-z0-9_.-]+)/g)) {
613
+ const bundleAlias = normalizeAlias(bundleMatch[1]);
614
+ const libraryAliases = catalog2?.bundles.get(bundleAlias) ?? catalog2?.bundles.get(bundleMatch[1]) ?? [];
615
+ for (const libraryAlias of libraryAliases) {
616
+ const normalizedLibraryAlias = normalizeAlias(libraryAlias);
617
+ const lib = catalog2?.libraries.get(normalizedLibraryAlias) ?? catalog2?.libraries.get(libraryAlias);
618
+ if (!lib?.module || !BILLING_MODULE.test(lib.module)) continue;
619
+ const version = lib.version ?? (lib.versionRef ? catalog2?.versions.get(lib.versionRef) : void 0);
620
+ if (!evidence.some((row) => row.file === relative && row.expression === bundleMatch[0] && row.module === lib.module)) {
621
+ evidence.push({
622
+ file: relative,
623
+ module: lib.module,
624
+ version,
625
+ expression: bundleMatch[0],
626
+ source: version ? "version_catalog" : "unresolved"
627
+ });
628
+ }
629
+ }
630
+ }
631
+ for (const aliasMatch of text.matchAll(/\blibs\.([A-Za-z0-9_.-]+)/g)) {
632
+ if (aliasMatch[1].startsWith("bundles.")) continue;
633
+ const alias = normalizeAlias(aliasMatch[1]);
634
+ const lib = catalog2?.libraries.get(alias) ?? catalog2?.libraries.get(aliasMatch[1]);
635
+ if (!lib?.module || !BILLING_MODULE.test(lib.module)) continue;
636
+ const version = lib.version ?? (lib.versionRef ? catalog2?.versions.get(lib.versionRef) : void 0);
637
+ if (!evidence.some((row) => row.file === relative && row.expression === aliasMatch[0])) {
638
+ evidence.push({
639
+ file: relative,
640
+ module: lib.module,
641
+ version,
642
+ expression: aliasMatch[0],
643
+ source: version ? "version_catalog" : "unresolved"
644
+ });
645
+ }
646
+ }
647
+ if (BILLING_MODULE.test(text) && !evidence.some((row) => row.file === relative)) {
648
+ evidence.push({
649
+ file: relative,
650
+ module: "com.android.billingclient:billing",
651
+ expression: "Billing dependency found but version could not be resolved",
652
+ source: "unresolved"
653
+ });
654
+ }
655
+ }
656
+ const detectedVersions = [...new Set(evidence.flatMap((row) => row.version ? [row.version] : []))].sort();
657
+ const policy = policyAt(now);
658
+ const majors = detectedVersions.map(majorOf).filter((major) => major !== null);
659
+ const unresolved = evidence.some((row) => !row.version);
660
+ let status;
661
+ let summary;
662
+ const actions = [];
663
+ if (evidence.length === 0) {
664
+ status = "not_used";
665
+ summary = "Google Play Billing dependency was not found in the scanned Gradle project.";
666
+ } else if (!policy.scheduleCurrent || policy.minimumSupportedMajor === null) {
667
+ status = "unresolved";
668
+ summary = `The official schedule embedded in this release ends at Billing Library ${policy.latestKnownMajor}; current policy must be refreshed from the source.`;
669
+ actions.push("Check the official Billing deprecation table and update Mimi Seed before relying on this result.");
670
+ } else if (majors.some((major) => major < policy.minimumSupportedMajor)) {
671
+ status = "blocker";
672
+ summary = `Billing Library ${detectedVersions.join(", ")} is below the submission minimum major ${policy.minimumSupportedMajor}.`;
673
+ actions.push(`Upgrade to a supported Billing Library before submitting a new app or update.`);
674
+ for (const major of [...new Set(majors.filter((value) => value < policy.minimumSupportedMajor))].sort()) {
675
+ const schedule = scheduleForMajor(major);
676
+ actions.push(schedule ? `Billing Library ${major}: standard deadline ${schedule.submissionDeadline}; extension deadline ${schedule.extensionDeadline} only if Google granted it in Play Console.` : `Billing Library ${major}: its deadline predates the embedded official table; no active extension should be assumed.`);
677
+ }
678
+ } else if (unresolved || majors.length === 0) {
679
+ status = "unresolved";
680
+ summary = "A Billing dependency was found, but at least one version expression could not be resolved statically.";
681
+ actions.push("Resolve the reported Gradle/version catalog expression or install the declared IAP package, then run the check again.");
682
+ } else if (majors.some((major) => major === policy.minimumSupportedMajor)) {
683
+ status = "warning";
684
+ summary = `Billing Library ${detectedVersions.join(", ")} is currently supported but is the next major scheduled for deprecation.`;
685
+ const nextDeadline = scheduleForMajor(policy.minimumSupportedMajor);
686
+ if (nextDeadline) actions.push(`Plan an upgrade before ${nextDeadline.submissionDeadline}.`);
687
+ } else {
688
+ status = "pass";
689
+ summary = `Billing Library ${detectedVersions.join(", ")} satisfies the current submission policy.`;
690
+ }
691
+ return {
692
+ projectPath: root,
693
+ checkedAt: now.toISOString(),
694
+ status,
695
+ detectedVersions,
696
+ evidence,
697
+ policy,
698
+ summary,
699
+ actions,
700
+ upgrade: {
701
+ installCommand: "android skills add play-billing-library-version-upgrade",
702
+ prompt: "Help me upgrade my Play Billing Library implementation.",
703
+ automaticExecution: false
704
+ }
705
+ };
706
+ }
707
+
708
+ // src/checks/release-doctor.ts
709
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
710
+ ".git",
711
+ ".gradle",
712
+ ".idea",
713
+ ".next",
714
+ ".expo",
715
+ "build",
716
+ "dist",
717
+ "node_modules",
718
+ "Pods",
719
+ "DerivedData"
720
+ ]);
721
+ var TARGET_SDK_POLICY = [
722
+ { effectiveDate: "2025-08-31", minimum: 35 },
723
+ { effectiveDate: "2026-08-31", minimum: 36 }
724
+ ];
725
+ var TARGET_SDK_SOURCE = "https://support.google.com/googleplay/android-developer/answer/11926878";
726
+ async function walk2(root, maxDepth = 7) {
727
+ const files = [];
728
+ async function visit(dir, depth) {
729
+ if (depth > maxDepth) return;
730
+ let entries;
731
+ try {
732
+ entries = await fs2.readdir(dir, { withFileTypes: true });
733
+ } catch {
734
+ return;
735
+ }
736
+ for (const entry of entries) {
737
+ const absolute = path2.join(dir, entry.name);
738
+ if (entry.isDirectory()) {
739
+ if (!SKIP_DIRS2.has(entry.name)) await visit(absolute, depth + 1);
740
+ continue;
741
+ }
742
+ if (!entry.isFile() || !isRelevantFile(entry.name)) continue;
743
+ try {
744
+ files.push({
745
+ absolute,
746
+ relative: path2.relative(root, absolute).replace(/\\/g, "/"),
747
+ text: await fs2.readFile(absolute, "utf8")
748
+ });
749
+ } catch {
750
+ }
751
+ }
752
+ }
753
+ await visit(root, 0);
754
+ return files;
755
+ }
756
+ function isRelevantFile(name) {
757
+ return name === "app.json" || name === "app.config.json" || /^app\.config\.(?:js|cjs|mjs|ts)$/.test(name) || name === "build.gradle" || name === "build.gradle.kts" || name === "libs.versions.toml" || name === "gradle.properties" || name === "AndroidManifest.xml" || name === "Info.plist" || name === "project.pbxproj" || name === "package.json";
758
+ }
759
+ function unique(values) {
760
+ return [...new Set(values.filter(Boolean))].sort();
761
+ }
762
+ function parseExpo(files) {
763
+ const androidPackageNames = [];
764
+ const iosBundleIds = [];
765
+ const platforms = /* @__PURE__ */ new Set();
766
+ let detected = false;
767
+ for (const file of files.filter((candidate) => /(?:^|\/)app(?:\.config)?\.(?:json|js|cjs|mjs|ts)$/.test(candidate.relative))) {
768
+ try {
769
+ const json = JSON.parse(file.text);
770
+ const hasExpoRoot = Boolean(json.expo && typeof json.expo === "object");
771
+ const expo = hasExpoRoot ? json.expo : json;
772
+ if (hasExpoRoot || typeof expo.android?.package === "string" || typeof expo.ios?.bundleIdentifier === "string" || Array.isArray(expo.platforms)) detected = true;
773
+ if (typeof expo.android?.package === "string") androidPackageNames.push(expo.android.package);
774
+ if (typeof expo.ios?.bundleIdentifier === "string") iosBundleIds.push(expo.ios.bundleIdentifier);
775
+ if (Array.isArray(expo.platforms)) {
776
+ for (const platform of expo.platforms) if (typeof platform === "string") platforms.add(platform);
777
+ }
778
+ } catch {
779
+ const android = file.text.match(/\bandroid\s*:\s*\{[\s\S]{0,3000}?\bpackage\s*:\s*['"]([^'"]+)['"]/);
780
+ const ios = file.text.match(/\bios\s*:\s*\{[\s\S]{0,3000}?\bbundleIdentifier\s*:\s*['"]([^'"]+)['"]/);
781
+ if (android?.[1] || ios?.[1] || /\bexpo\s*:/.test(file.text)) detected = true;
782
+ if (android?.[1]) androidPackageNames.push(android[1]);
783
+ if (ios?.[1]) iosBundleIds.push(ios[1]);
784
+ }
785
+ }
786
+ for (const file of files.filter((candidate) => candidate.relative.endsWith("package.json"))) {
787
+ try {
788
+ const manifest = JSON.parse(file.text);
789
+ const groups = ["dependencies", "devDependencies"].map((key) => manifest[key]);
790
+ if (groups.some((group) => group && typeof group === "object" && "expo" in group)) detected = true;
791
+ } catch {
792
+ }
793
+ }
794
+ return { androidPackageNames, iosBundleIds, platforms, detected };
795
+ }
796
+ function detectProject(files) {
797
+ const expo = parseExpo(files);
798
+ const gradleFiles = files.filter((file) => /build\.gradle(?:\.kts)?$/.test(file.relative));
799
+ const pbxFiles = files.filter((file) => file.relative.endsWith("project.pbxproj"));
800
+ const plistFiles = files.filter((file) => file.relative.endsWith("Info.plist"));
801
+ const androidAppGradleFiles = gradleFiles.filter((file) => /\bcom\.android\.application\b|\blibs\.plugins\.android\.application\b|\bapplicationId\b/.test(file.text));
802
+ const androidAppManifestFiles = files.filter((file) => /(?:^|\/)android\/app\/src\/main\/AndroidManifest\.xml$/.test(file.relative));
803
+ const versionCatalogFiles = files.filter((file) => file.relative.endsWith("libs.versions.toml"));
804
+ const iosPbxFiles = pbxFiles.filter((file) => /(?:^|\/)ios\//.test(file.relative) || /\b(?:SDKROOT\s*=\s*iphoneos|IPHONEOS_DEPLOYMENT_TARGET|TARGETED_DEVICE_FAMILY)\b/.test(file.text));
805
+ const iosPlistFiles = plistFiles.filter((file) => /(?:^|\/)ios\//.test(file.relative) || iosPbxFiles.length > 0);
806
+ const androidPackageNames = [...expo.androidPackageNames];
807
+ for (const file of androidAppGradleFiles) {
808
+ for (const match of file.text.matchAll(/\bapplicationId\s*(?:=\s*)?["']([^"']+)["']/g)) {
809
+ androidPackageNames.push(match[1]);
810
+ }
811
+ }
812
+ const iosBundleIds = [...expo.iosBundleIds];
813
+ for (const file of iosPbxFiles) {
814
+ for (const match of file.text.matchAll(/PRODUCT_BUNDLE_IDENTIFIER\s*=\s*([^;]+);/g)) {
815
+ const value = match[1].trim().replace(/^["']|["']$/g, "");
816
+ if (value && !value.includes("$") && !/(?:^|\.)(?:Tests?|UITests?|RunnerTests)$/i.test(value)) {
817
+ iosBundleIds.push(value);
818
+ }
819
+ }
820
+ }
821
+ for (const file of iosPlistFiles) {
822
+ const match = file.text.match(/<key>CFBundleIdentifier<\/key>\s*<string>([^<]+)<\/string>/);
823
+ if (match?.[1] && !match[1].includes("$")) iosBundleIds.push(match[1]);
824
+ }
825
+ const expoTargetsAndroid = expo.detected && (expo.platforms.size === 0 || expo.platforms.has("android"));
826
+ const expoTargetsIos = expo.detected && (expo.platforms.size === 0 || expo.platforms.has("ios"));
827
+ const android = androidAppGradleFiles.length > 0 || expo.androidPackageNames.length > 0 || expoTargetsAndroid || androidAppManifestFiles.length > 0;
828
+ const ios = iosPbxFiles.length > 0 || iosPlistFiles.some((file) => /(?:^|\/)ios\//.test(file.relative)) || expo.iosBundleIds.length > 0 || expoTargetsIos;
829
+ const androidGradleFiles = gradleFiles.filter((file) => androidAppGradleFiles.includes(file) || /(?:^|\/)android\//.test(file.relative));
830
+ return {
831
+ android,
832
+ ios,
833
+ androidPackageNames: unique(androidPackageNames),
834
+ iosBundleIds: unique(iosBundleIds),
835
+ gradleFiles: [...androidGradleFiles, ...versionCatalogFiles]
836
+ };
837
+ }
838
+ function targetPolicy(now) {
839
+ const today = now.toISOString().slice(0, 10);
840
+ const current = [...TARGET_SDK_POLICY].filter((row) => row.effectiveDate <= today).at(-1);
841
+ const refreshAfter = `${Number(TARGET_SDK_POLICY.at(-1).effectiveDate.slice(0, 4)) + 1}-08-31`;
842
+ return {
843
+ minimum: current?.minimum ?? null,
844
+ scheduleCurrent: today <= refreshAfter,
845
+ effectiveDate: current?.effectiveDate
846
+ };
847
+ }
848
+ function targetSdkFindings(gradleFiles, now) {
849
+ const evidence = [];
850
+ let hasUnresolvedExpression = false;
851
+ const catalogs = /* @__PURE__ */ new Map();
852
+ for (const file of gradleFiles.filter((candidate) => candidate.relative.endsWith("libs.versions.toml"))) {
853
+ let section2 = "";
854
+ for (const rawLine of file.text.split(/\r?\n/)) {
855
+ const line = rawLine.replace(/\s+#.*$/, "").trim();
856
+ const sectionMatch = line.match(/^\[([^\]]+)]$/);
857
+ if (sectionMatch) {
858
+ section2 = sectionMatch[1];
859
+ continue;
860
+ }
861
+ if (section2 !== "versions") continue;
862
+ const version = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*["'](\d+)["']/);
863
+ if (version) catalogs.set(version[1], { value: Number.parseInt(version[2], 10), file: file.relative });
864
+ }
865
+ }
866
+ for (const file of gradleFiles.filter((candidate) => /build\.gradle(?:\.kts)?$/.test(candidate.relative))) {
867
+ for (const match of file.text.matchAll(/\btargetSdk(?:Version)?\s*(?:=\s*)?(\d+)/g)) {
868
+ evidence.push({ file: file.relative, value: Number.parseInt(match[1], 10) });
869
+ }
870
+ for (const match of file.text.matchAll(/\btargetSdk(?:Version)?\s*(?:=\s*)?libs\.versions\.([A-Za-z0-9_.-]+?)(?=\.get\(\)|\s|$)/g)) {
871
+ const resolved = catalogs.get(match[1]) ?? catalogs.get(match[1].replace(/\./g, "-"));
872
+ if (resolved) evidence.push({ file: resolved.file, value: resolved.value });
873
+ }
874
+ if (/\btargetSdk(?:Version)?\b/.test(file.text) && !/\btargetSdk(?:Version)?\s*(?:=\s*)?\d+/.test(file.text)) {
875
+ const catalogExpression = /\btargetSdk(?:Version)?\s*(?:=\s*)?libs\.versions\.([A-Za-z0-9_.-]+?)(?=\.get\(\)|\s|$)/.exec(file.text);
876
+ const resolved = catalogExpression ? catalogs.get(catalogExpression[1]) ?? catalogs.get(catalogExpression[1].replace(/\./g, "-")) : void 0;
877
+ if (!resolved) hasUnresolvedExpression = true;
878
+ }
879
+ }
880
+ const policy = targetPolicy(now);
881
+ if (!policy.scheduleCurrent || policy.minimum === null) {
882
+ return [{
883
+ code: "TARGET_SDK_POLICY_REFRESH_REQUIRED",
884
+ severity: "warning",
885
+ title: "Target API policy table needs a refresh",
886
+ detail: "The embedded Google Play Target API schedule is no longer current enough for a definitive result.",
887
+ action: "Check the current Google Play Target API requirement before submission.",
888
+ sourceUrl: TARGET_SDK_SOURCE,
889
+ ko: {
890
+ title: "Target API \uC815\uCC45\uD45C \uAC31\uC2E0 \uD544\uC694",
891
+ detail: "\uB0B4\uC7A5\uB41C Google Play Target API \uC77C\uC815\uB9CC\uC73C\uB85C\uB294 \uD604\uC7AC \uC81C\uCD9C \uC694\uAC74\uC744 \uD655\uC815\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
892
+ action: "\uC81C\uCD9C \uC804\uC5D0 \uCD5C\uC2E0 Google Play Target API \uC694\uAD6C\uC0AC\uD56D\uC744 \uD655\uC778\uD558\uC138\uC694."
893
+ }
894
+ }];
895
+ }
896
+ if (evidence.length === 0) {
897
+ return [{
898
+ code: "TARGET_SDK_UNRESOLVED",
899
+ severity: "warning",
900
+ title: "Android targetSdk could not be resolved locally",
901
+ detail: hasUnresolvedExpression ? "A targetSdk expression exists, but its numeric value is defined indirectly." : "No literal targetSdk value was found in the scanned Gradle files.",
902
+ action: `Resolve the release variant and confirm targetSdk ${policy.minimum} or newer before submission.`,
903
+ sourceUrl: TARGET_SDK_SOURCE,
904
+ ko: {
905
+ title: "Android targetSdk \uAC12\uC744 \uB85C\uCEEC\uC5D0\uC11C \uD655\uC815\uD558\uC9C0 \uBABB\uD568",
906
+ detail: hasUnresolvedExpression ? "targetSdk \uD45C\uD604\uC2DD\uC740 \uC788\uC9C0\uB9CC \uC22B\uC790 \uAC12\uC774 \uB2E4\uB978 \uD30C\uC77C\uC774\uB098 \uBCC0\uC218\uC5D0 \uC815\uC758\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4." : "\uAC80\uC0AC\uD55C Gradle \uD30C\uC77C\uC5D0\uC11C \uC22B\uC790\uB85C \uB41C targetSdk \uAC12\uC744 \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
907
+ action: `\uB9B4\uB9AC\uC2A4 variant\uC758 \uAC12\uC744 \uD655\uC778\uD574 targetSdk ${policy.minimum} \uC774\uC0C1\uC778\uC9C0 \uAC80\uC99D\uD558\uC138\uC694.`
908
+ }
909
+ }];
910
+ }
911
+ const below = evidence.filter((row) => row.value < policy.minimum);
912
+ if (below.length > 0) {
913
+ const first = below.sort((left, right) => left.value - right.value)[0];
914
+ return [{
915
+ code: "TARGET_SDK_BELOW_MINIMUM",
916
+ severity: "blocker",
917
+ title: `Android targetSdk ${first.value} is below the submission minimum`,
918
+ detail: `Google Play requires targetSdk ${policy.minimum} or newer for new apps and updates after ${policy.effectiveDate}.`,
919
+ action: `Upgrade targetSdk to ${policy.minimum} or newer and test the release build.`,
920
+ file: first.file,
921
+ sourceUrl: TARGET_SDK_SOURCE,
922
+ ko: {
923
+ title: `Android targetSdk ${first.value}\uC740 \uD604\uC7AC \uC81C\uCD9C \uAE30\uC900 \uBBF8\uB2EC`,
924
+ detail: `${policy.effectiveDate} \uC774\uD6C4 \uC2E0\uADDC \uC571\uACFC \uC5C5\uB370\uC774\uD2B8\uB294 targetSdk ${policy.minimum} \uC774\uC0C1\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4.`,
925
+ action: `targetSdk\uB97C ${policy.minimum} \uC774\uC0C1\uC73C\uB85C \uC62C\uB9AC\uACE0 \uB9B4\uB9AC\uC2A4 \uBE4C\uB4DC\uB97C \uD14C\uC2A4\uD2B8\uD558\uC138\uC694.`
926
+ }
927
+ }];
928
+ }
929
+ const lowest = evidence.sort((left, right) => left.value - right.value)[0];
930
+ return [{
931
+ code: "TARGET_SDK_OK",
932
+ severity: "info",
933
+ title: `Android targetSdk ${lowest.value} meets the current minimum`,
934
+ detail: `The lowest literal targetSdk found is at least ${policy.minimum}.`,
935
+ file: lowest.file,
936
+ sourceUrl: TARGET_SDK_SOURCE,
937
+ ko: {
938
+ title: `Android targetSdk ${lowest.value}\uC740 \uD604\uC7AC \uC81C\uCD9C \uAE30\uC900 \uCDA9\uC871`,
939
+ detail: `\uAC10\uC9C0\uB41C \uAC00\uC7A5 \uB0AE\uC740 targetSdk\uAC00 \uD604\uC7AC \uCD5C\uC18C\uAC12 ${policy.minimum} \uC774\uC0C1\uC785\uB2C8\uB2E4.`
940
+ }
941
+ }];
942
+ }
943
+ function hasSpecializedAndroidProfile(files) {
944
+ return files.filter((file) => file.relative.endsWith("AndroidManifest.xml")).some((file) => {
945
+ const manifest = file.text.replace(/<!--[\s\S]*?-->/g, "");
946
+ return /android\.hardware\.type\.(?:watch|automotive)|android\.(?:software|hardware)\.xr|LEANBACK_LAUNCHER/i.test(manifest) || /android\.software\.leanback[^>]*android:required\s*=\s*["']true["']/i.test(manifest);
947
+ });
948
+ }
949
+ async function scanReleaseDoctor(projectPath, now = /* @__PURE__ */ new Date()) {
950
+ const root = path2.resolve(projectPath);
951
+ let stat;
952
+ try {
953
+ stat = await fs2.stat(root);
954
+ } catch {
955
+ throw new Error(`Project path does not exist or is not readable: ${root}`);
956
+ }
957
+ if (!stat.isDirectory()) throw new Error(`Project path is not a directory: ${root}`);
958
+ const files = await walk2(root);
959
+ const detected = detectProject(files);
960
+ const platforms = [];
961
+ if (detected.android) platforms.push("android");
962
+ if (detected.ios) platforms.push("ios");
963
+ const findings = [];
964
+ if (platforms.length === 0) {
965
+ findings.push({
966
+ code: "NO_MOBILE_PROJECT",
967
+ severity: "blocker",
968
+ title: "No Android or iOS app project was detected",
969
+ detail: "Release Doctor could not find an Expo config, Android Gradle app, Xcode project, or iOS Info.plist.",
970
+ action: "Run this command from the mobile app repository root.",
971
+ ko: {
972
+ title: "Android \uB610\uB294 iOS \uC571 \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC9C0 \uBABB\uD568",
973
+ detail: "Expo \uC124\uC815, Android Gradle \uC571, Xcode \uD504\uB85C\uC81D\uD2B8, iOS Info.plist\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
974
+ action: "\uBAA8\uBC14\uC77C \uC571 \uC800\uC7A5\uC18C\uC758 \uB8E8\uD2B8\uC5D0\uC11C \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694."
975
+ }
976
+ });
977
+ }
978
+ if (detected.android) {
979
+ if (detected.androidPackageNames.length === 0) {
980
+ findings.push({
981
+ code: "ANDROID_PACKAGE_UNRESOLVED",
982
+ severity: "warning",
983
+ title: "Android application ID could not be resolved",
984
+ detail: "The Android project was detected, but no literal applicationId or Expo android.package was found.",
985
+ action: "Confirm the release variant applicationId before connecting Google Play.",
986
+ ko: {
987
+ title: "Android application ID\uB97C \uD655\uC815\uD558\uC9C0 \uBABB\uD568",
988
+ detail: "Android \uD504\uB85C\uC81D\uD2B8\uB294 \uAC10\uC9C0\uD588\uC9C0\uB9CC applicationId \uB610\uB294 Expo android.package\uC758 \uBB38\uC790\uC5F4 \uAC12\uC744 \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
989
+ action: "Google Play \uC5F0\uACB0 \uC804\uC5D0 \uB9B4\uB9AC\uC2A4 variant\uC758 applicationId\uB97C \uD655\uC778\uD558\uC138\uC694."
990
+ }
991
+ });
992
+ } else {
993
+ findings.push({
994
+ code: "ANDROID_PACKAGE_FOUND",
995
+ severity: "info",
996
+ title: "Android application ID detected",
997
+ detail: detected.androidPackageNames.join(", "),
998
+ ko: {
999
+ title: "Android application ID \uAC10\uC9C0 \uC644\uB8CC",
1000
+ detail: detected.androidPackageNames.join(", ")
1001
+ }
1002
+ });
1003
+ if (detected.androidPackageNames.length > 1) {
1004
+ findings.push({
1005
+ code: "MULTIPLE_ANDROID_APPLICATION_IDS",
1006
+ severity: "warning",
1007
+ title: "Multiple Android application IDs share this scan scope",
1008
+ detail: "Repository-wide Target API and Billing evidence may belong to different apps or variants.",
1009
+ action: "Run Release Doctor with --path set to one mobile app root before treating blockers as app-specific.",
1010
+ ko: {
1011
+ title: "\uAC80\uC0AC \uBC94\uC704\uC5D0 Android application ID\uAC00 \uC5EC\uB7EC \uAC1C \uC788\uC74C",
1012
+ detail: "\uC800\uC7A5\uC18C \uC804\uCCB4\uC5D0\uC11C \uCC3E\uC740 Target API\uC640 Billing \uADFC\uAC70\uAC00 \uC11C\uB85C \uB2E4\uB978 \uC571 \uB610\uB294 variant\uC5D0 \uC18D\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
1013
+ action: "\uBE14\uB85C\uCEE4\uB97C \uD2B9\uC815 \uC571\uC758 \uACB0\uACFC\uB85C \uD310\uB2E8\uD558\uAE30 \uC804\uC5D0 --path\uB85C \uBAA8\uBC14\uC77C \uC571 \uB8E8\uD2B8\uB97C \uD558\uB098\uB9CC \uC9C0\uC815\uD574 \uB2E4\uC2DC \uAC80\uC0AC\uD558\uC138\uC694."
1014
+ }
1015
+ });
1016
+ }
1017
+ }
1018
+ if (hasSpecializedAndroidProfile(files)) {
1019
+ findings.push({
1020
+ code: "TARGET_SDK_SPECIALIZED_APP_REVIEW",
1021
+ severity: "warning",
1022
+ title: "Specialized Android app type needs a category-specific Target API check",
1023
+ detail: "Wear OS, Android TV, Android Automotive OS, or Android XR evidence was found. Their submission minimums differ from general mobile apps, so Release Doctor did not apply the generic API 36 rule.",
1024
+ action: "Confirm the app category and its current Target API requirement in the official table.",
1025
+ sourceUrl: TARGET_SDK_SOURCE,
1026
+ ko: {
1027
+ title: "\uD2B9\uC218 Android \uC571 \uC720\uD615\uC740 \uCE74\uD14C\uACE0\uB9AC\uBCC4 Target API \uD655\uC778 \uD544\uC694",
1028
+ detail: "Wear OS, Android TV, Android Automotive OS \uB610\uB294 Android XR \uADFC\uAC70\uB97C \uCC3E\uC558\uC2B5\uB2C8\uB2E4. \uC77C\uBC18 \uBAA8\uBC14\uC77C \uC571\uACFC \uC81C\uCD9C \uCD5C\uC18C\uAC12\uC774 \uB2EC\uB77C API 36 \uAE30\uC900\uC744 \uC77C\uAD04 \uC801\uC6A9\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.",
1029
+ action: "\uC571 \uCE74\uD14C\uACE0\uB9AC\uC640 \uD574\uB2F9 Target API \uC694\uAD6C\uC0AC\uD56D\uC744 \uACF5\uC2DD \uD45C\uC5D0\uC11C \uD655\uC778\uD558\uC138\uC694."
1030
+ }
1031
+ });
1032
+ } else {
1033
+ findings.push(...targetSdkFindings(detected.gradleFiles, now));
1034
+ }
1035
+ const billing = await checkBillingCompliance(root, now);
1036
+ if (billing.status !== "not_used") {
1037
+ const billingKoDetail = billing.status === "pass" ? `\uAC10\uC9C0\uB41C Billing Library ${billing.detectedVersions.join(", ")}\uC740 \uD604\uC7AC \uC81C\uCD9C \uAE30\uC900\uC744 \uCDA9\uC871\uD569\uB2C8\uB2E4.` : billing.status === "blocker" ? `\uAC10\uC9C0\uB41C Billing Library ${billing.detectedVersions.join(", ")}\uC740 \uD604\uC7AC \uC81C\uCD9C \uCD5C\uC18C \uBC84\uC804\uBCF4\uB2E4 \uB0AE\uC2B5\uB2C8\uB2E4.` : billing.status === "warning" ? `\uAC10\uC9C0\uB41C Billing Library ${billing.detectedVersions.join(", ")}\uC740 \uD604\uC7AC \uC9C0\uC6D0\uB418\uC9C0\uB9CC \uB2E4\uC74C \uC9C0\uC6D0 \uC885\uB8CC \uB300\uC0C1\uC785\uB2C8\uB2E4.` : "Billing \uC758\uC874\uC131\uC740 \uCC3E\uC558\uC9C0\uB9CC \uBC84\uC804\uC744 \uC815\uC801\uC73C\uB85C \uD655\uC815\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.";
1038
+ findings.push({
1039
+ code: `BILLING_${billing.status.toUpperCase()}`,
1040
+ severity: billing.status === "blocker" ? "blocker" : billing.status === "pass" ? "info" : "warning",
1041
+ title: billing.status === "pass" ? "Google Play Billing version is supported" : "Google Play Billing needs attention",
1042
+ detail: billing.summary,
1043
+ action: billing.actions[0] ?? billing.upgrade.prompt,
1044
+ file: billing.evidence[0]?.file,
1045
+ sourceUrl: billing.policy.sourceUrl,
1046
+ ko: {
1047
+ title: billing.status === "pass" ? "Google Play Billing \uBC84\uC804 \uAE30\uC900 \uCDA9\uC871" : "Google Play Billing \uD655\uC778 \uD544\uC694",
1048
+ detail: billingKoDetail,
1049
+ action: billing.actions[0] ? "\uACF5\uC2DD \uB9C8\uAC10\uC77C\uACFC \uBCF4\uACE0\uB41C Gradle \uADFC\uAC70\uB97C \uD655\uC778\uD55C \uB4A4 \uC9C0\uC6D0 \uBC84\uC804\uC73C\uB85C \uC5C5\uADF8\uB808\uC774\uB4DC\uD558\uC138\uC694." : "Google Play Billing \uC5C5\uADF8\uB808\uC774\uB4DC Skill\uC744 \uC0AC\uC6A9\uD574 \uBCC0\uACBD\uC0AC\uD56D\uC744 \uAC80\uD1A0\uD558\uC138\uC694."
1050
+ }
1051
+ });
1052
+ }
1053
+ }
1054
+ if (detected.ios) {
1055
+ if (detected.iosBundleIds.length === 0) {
1056
+ findings.push({
1057
+ code: "IOS_BUNDLE_ID_UNRESOLVED",
1058
+ severity: "warning",
1059
+ title: "iOS bundle identifier could not be resolved",
1060
+ detail: "The iOS project was detected, but no literal bundle identifier was found in Expo config, Info.plist, or project.pbxproj.",
1061
+ action: "Confirm PRODUCT_BUNDLE_IDENTIFIER for the release configuration before connecting App Store Connect.",
1062
+ ko: {
1063
+ title: "iOS bundle identifier\uB97C \uD655\uC815\uD558\uC9C0 \uBABB\uD568",
1064
+ detail: "iOS \uD504\uB85C\uC81D\uD2B8\uB294 \uAC10\uC9C0\uD588\uC9C0\uB9CC Expo \uC124\uC815, Info.plist, project.pbxproj\uC5D0\uC11C \uBB38\uC790\uC5F4 bundle identifier\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
1065
+ action: "App Store Connect \uC5F0\uACB0 \uC804\uC5D0 \uB9B4\uB9AC\uC2A4 \uAD6C\uC131\uC758 PRODUCT_BUNDLE_IDENTIFIER\uB97C \uD655\uC778\uD558\uC138\uC694."
1066
+ }
1067
+ });
1068
+ } else {
1069
+ findings.push({
1070
+ code: "IOS_BUNDLE_ID_FOUND",
1071
+ severity: "info",
1072
+ title: "iOS bundle identifier detected",
1073
+ detail: detected.iosBundleIds.join(", "),
1074
+ ko: {
1075
+ title: "iOS bundle identifier \uAC10\uC9C0 \uC644\uB8CC",
1076
+ detail: detected.iosBundleIds.join(", ")
1077
+ }
1078
+ });
1079
+ if (detected.iosBundleIds.length > 1) {
1080
+ findings.push({
1081
+ code: "MULTIPLE_IOS_BUNDLE_IDS",
1082
+ severity: "warning",
1083
+ title: "Multiple iOS bundle identifiers share this scan scope",
1084
+ detail: "The identifiers may represent multiple apps, extensions, or release targets in the same repository.",
1085
+ action: "Confirm which identifier belongs to the release target; use --path to narrow a monorepo scan.",
1086
+ ko: {
1087
+ title: "\uAC80\uC0AC \uBC94\uC704\uC5D0 iOS bundle identifier\uAC00 \uC5EC\uB7EC \uAC1C \uC788\uC74C",
1088
+ detail: "\uAC19\uC740 \uC800\uC7A5\uC18C\uC758 \uC5EC\uB7EC \uC571, extension \uB610\uB294 \uCD9C\uC2DC \uD0C0\uAE43 \uC2DD\uBCC4\uC790\uAC00 \uD568\uAED8 \uAC10\uC9C0\uB410\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
1089
+ action: "\uCD9C\uC2DC \uD0C0\uAE43\uC758 \uC2DD\uBCC4\uC790\uB97C \uD655\uC778\uD558\uACE0, \uBAA8\uB178\uB808\uD3EC\uB77C\uBA74 --path\uB85C \uAC80\uC0AC \uBC94\uC704\uB97C \uC881\uD788\uC138\uC694."
1090
+ }
1091
+ });
1092
+ }
1093
+ }
1094
+ }
1095
+ const counts = findings.reduce(
1096
+ (result, finding) => ({ ...result, [finding.severity]: result[finding.severity] + 1 }),
1097
+ { blocker: 0, warning: 0, info: 0 }
1098
+ );
1099
+ return {
1100
+ projectPath: root,
1101
+ checkedAt: now.toISOString(),
1102
+ platforms,
1103
+ identifiers: {
1104
+ androidPackageNames: detected.androidPackageNames,
1105
+ iosBundleIds: detected.iosBundleIds
1106
+ },
1107
+ counts,
1108
+ findings,
1109
+ coverage: {
1110
+ checked: [
1111
+ "mobile project and app identifier detection",
1112
+ ...detected.android ? ["Android targetSdk policy", "Google Play Billing dependency policy"] : []
1113
+ ],
1114
+ requiresStoreConnection: [
1115
+ "store listing metadata and screenshots",
1116
+ "uploaded build availability and processing state",
1117
+ "App Store age-rating answers and Google Play declarations",
1118
+ "review submission and rollout status"
1119
+ ]
1120
+ }
1121
+ };
1122
+ }
1123
+
1124
+ // src/check.ts
263
1125
  var M2 = catalog(
264
1126
  {
265
1127
  noAccount: "\uC5F0\uACB0\uB41C \uACC4\uC815 \uC5C6\uC74C. `mimi-seed init` \uC2E4\uD589.\n",
1128
+ missingOptionValue: (option) => `${option} \uB4A4\uC5D0 \uAC12\uC744 \uC785\uB825\uD558\uC138\uC694.`,
1129
+ unknownOption: (option) => `\uC54C \uC218 \uC5C6\uB294 check \uC635\uC158: ${option}`,
1130
+ unexpectedArgument: (value) => `\uC608\uC0C1\uD558\uC9C0 \uC54A\uC740 check \uC778\uC790: ${value}`,
1131
+ localAppConflict: "--app\uC740 \uC6D0\uACA9 \uAC80\uC0AC \uC804\uC6A9\uC774\uBBC0\uB85C --local, --path, --json\uACFC \uD568\uAED8 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.\n",
1132
+ localFailed: (message) => `Release Doctor \uC2E4\uD589 \uC2E4\uD328: ${message}
1133
+ `,
266
1134
  title: "mimi-seed check \u2014 \uCD9C\uC2DC \uC804 \uC810\uAC80\n\n",
267
1135
  appsFailed: (msg) => `\uC571 \uBAA9\uB85D \uC870\uD68C \uC2E4\uD328: ${msg}
268
1136
  `,
@@ -294,6 +1162,12 @@ var M2 = catalog(
294
1162
  },
295
1163
  {
296
1164
  noAccount: "No account connected. Run `mimi-seed init`.\n",
1165
+ missingOptionValue: (option) => `Provide a value after ${option}.`,
1166
+ unknownOption: (option) => `Unknown check option: ${option}`,
1167
+ unexpectedArgument: (value) => `Unexpected check argument: ${value}`,
1168
+ localAppConflict: "--app is remote-only and cannot be combined with --local, --path, or --json.\n",
1169
+ localFailed: (message) => `Release Doctor failed: ${message}
1170
+ `,
297
1171
  title: "mimi-seed check \u2014 pre-launch check\n\n",
298
1172
  appsFailed: (msg) => `Failed to list apps: ${msg}
299
1173
  `,
@@ -324,7 +1198,7 @@ Score: ${bar}
324
1198
  `
325
1199
  }
326
1200
  );
327
- function parseArgs(argv) {
1201
+ function parseCheckArgs(argv) {
328
1202
  const args = {
329
1203
  projectPath: process.cwd(),
330
1204
  projectPathExplicit: false,
@@ -332,15 +1206,22 @@ function parseArgs(argv) {
332
1206
  local: false,
333
1207
  json: false
334
1208
  };
1209
+ const takeValue = (option, index) => {
1210
+ const value = argv[index + 1];
1211
+ if (!value || value.startsWith("--")) throw new Error(M2().missingOptionValue(option));
1212
+ return value;
1213
+ };
335
1214
  for (let i = 0; i < argv.length; i++) {
336
- if (argv[i] === "--app" && argv[i + 1]) args.appId = argv[++i];
337
- if (argv[i] === "--path" && argv[i + 1]) {
338
- args.projectPath = argv[++i];
1215
+ const token = argv[i];
1216
+ if (token === "--app") args.appId = takeValue(token, i++);
1217
+ else if (token === "--path") {
1218
+ args.projectPath = takeValue(token, i++);
339
1219
  args.projectPathExplicit = true;
340
- }
341
- if (argv[i] === "--fail-on-blocker") args.failOnBlocker = true;
342
- if (argv[i] === "--local") args.local = true;
343
- if (argv[i] === "--json") args.json = true;
1220
+ } else if (token === "--fail-on-blocker") args.failOnBlocker = true;
1221
+ else if (token === "--local") args.local = true;
1222
+ else if (token === "--json") args.json = true;
1223
+ else if (token.startsWith("-")) throw new Error(M2().unknownOption(token));
1224
+ else throw new Error(M2().unexpectedArgument(token));
344
1225
  }
345
1226
  return args;
346
1227
  }
@@ -357,15 +1238,37 @@ var MODULE_LABELS = {
357
1238
  checklist: "Checklist"
358
1239
  };
359
1240
  async function cmdCheck(argv) {
360
- const args = parseArgs(argv);
1241
+ let args;
1242
+ try {
1243
+ args = parseCheckArgs(argv);
1244
+ } catch (error) {
1245
+ process.stderr.write(kleur2.red(`${error instanceof Error ? error.message : String(error)}
1246
+ `));
1247
+ process.exitCode = 2;
1248
+ return;
1249
+ }
361
1250
  const cfg = await getEffectiveConfig();
362
1251
  const localRequested = args.local || args.json || args.projectPathExplicit;
1252
+ if (args.appId && localRequested) {
1253
+ process.stderr.write(kleur2.red(M2().localAppConflict));
1254
+ process.exitCode = 2;
1255
+ return;
1256
+ }
1257
+ if (args.appId && !cfg) {
1258
+ process.stderr.write(kleur2.red(M2().noAccount));
1259
+ process.exitCode = 1;
1260
+ return;
1261
+ }
363
1262
  if (localRequested || !cfg) {
364
- const doctorArgs = [args.projectPath];
365
- if (args.json) doctorArgs.push("--json");
366
- if (args.failOnBlocker) doctorArgs.push("--fail-on-blocker");
367
- const exitCode = await runMcpBin("mimi-seed-release-doctor", doctorArgs);
368
- if (exitCode !== 0) process.exit(exitCode);
1263
+ try {
1264
+ const report = await scanReleaseDoctor(args.projectPath);
1265
+ process.stdout.write(args.json ? `${JSON.stringify(report, null, 2)}
1266
+ ` : renderReleaseDoctor(report, resolveLang()));
1267
+ if (args.failOnBlocker && report.counts.blocker > 0) process.exitCode = 1;
1268
+ } catch (error) {
1269
+ process.stderr.write(kleur2.red(M2().localFailed(error instanceof Error ? error.message : String(error))));
1270
+ process.exitCode = 2;
1271
+ }
369
1272
  return;
370
1273
  }
371
1274
  process.stdout.write(kleur2.bold(M2().title));
@@ -571,7 +1474,7 @@ Update now.`,
571
1474
  `
572
1475
  }
573
1476
  );
574
- function parseArgs2(argv) {
1477
+ function parseArgs(argv) {
575
1478
  const args = { to: "HEAD", locales: ["ko", "en-US"], apply: false, noInteractive: false, limit: 30 };
576
1479
  for (let i = 0; i < argv.length; i++) {
577
1480
  if (argv[i] === "--from" && argv[i + 1]) args.from = argv[++i];
@@ -628,7 +1531,7 @@ function parseFirstApp(text) {
628
1531
  return null;
629
1532
  }
630
1533
  async function cmdNotes(argv) {
631
- const args = parseArgs2(argv);
1534
+ const args = parseArgs(argv);
632
1535
  const cwd = process.cwd();
633
1536
  process.stdout.write(kleur3.bold(M3().title));
634
1537
  if (!isGitRepo(cwd)) {
@@ -837,7 +1740,7 @@ Respond with JSON only: { "reply": "the reply text" }`,
837
1740
  posted: "\u2713 Reply posted\n"
838
1741
  }
839
1742
  );
840
- function parseArgs3(argv) {
1743
+ function parseArgs2(argv) {
841
1744
  const args = { tone: "friendly", language: "ko", apply: false, noInteractive: false };
842
1745
  for (let i = 0; i < argv.length; i++) {
843
1746
  if (argv[i] === "--text" && argv[i + 1]) args.text = argv[++i];
@@ -894,7 +1797,7 @@ async function generateReply(opts) {
894
1797
  return parsed.reply;
895
1798
  }
896
1799
  async function cmdReview(argv) {
897
- const args = parseArgs3(argv);
1800
+ const args = parseArgs2(argv);
898
1801
  process.stdout.write(kleur4.bold(M4().title));
899
1802
  if (!process.env.ANTHROPIC_API_KEY) {
900
1803
  process.stdout.write(kleur4.red(M4().noApiKey));
@@ -1161,9 +2064,9 @@ async function cmdGa4(args) {
1161
2064
 
1162
2065
  // src/mcp-restart.ts
1163
2066
  import { execSync as execSync2 } from "child_process";
1164
- import fs from "fs";
2067
+ import fs3 from "fs";
1165
2068
  import os from "os";
1166
- import path from "path";
2069
+ import path3 from "path";
1167
2070
  import kleur7 from "kleur";
1168
2071
  function log3(msg) {
1169
2072
  process.stdout.write(msg + "\n");
@@ -1224,7 +2127,7 @@ var M6 = catalog(
1224
2127
  );
1225
2128
  function readJson(filePath) {
1226
2129
  try {
1227
- return JSON.parse(fs.readFileSync(filePath, "utf8"));
2130
+ return JSON.parse(fs3.readFileSync(filePath, "utf8"));
1228
2131
  } catch {
1229
2132
  return {};
1230
2133
  }
@@ -1264,8 +2167,8 @@ function collectServers() {
1264
2167
  for (const [name, cfg] of entries) if (!(name in servers)) servers[name] = cfg;
1265
2168
  };
1266
2169
  const projectDir = process.cwd();
1267
- add(readJson(path.join(projectDir, ".mcp.json")).mcpServers, ".mcp.json");
1268
- const home = readJson(path.join(os.homedir(), ".claude.json"));
2170
+ add(readJson(path3.join(projectDir, ".mcp.json")).mcpServers, ".mcp.json");
2171
+ const home = readJson(path3.join(os.homedir(), ".claude.json"));
1269
2172
  const projects = home.projects;
1270
2173
  add(projects?.[projectDir]?.mcpServers, "~/.claude.json (projects)");
1271
2174
  add(home.mcpServers, "~/.claude.json");
@@ -1302,11 +2205,11 @@ function findPids(markers) {
1302
2205
  const pid = Number(m[1]);
1303
2206
  if (skip.has(pid)) continue;
1304
2207
  const argv = m[2].split(/\s+/).filter(Boolean);
1305
- const exeBase = path.basename(argv[0] ?? "");
2208
+ const exeBase = path3.basename(argv[0] ?? "");
1306
2209
  const rest = argv.slice(1);
1307
2210
  const hit = markers.some((marker) => {
1308
- const base = path.basename(marker);
1309
- return exeBase === base || rest.includes(marker) || rest.some((a) => path.basename(a) === base);
2211
+ const base = path3.basename(marker);
2212
+ return exeBase === base || rest.includes(marker) || rest.some((a) => path3.basename(a) === base);
1310
2213
  });
1311
2214
  if (hit) pids.push(pid);
1312
2215
  }
@@ -1392,8 +2295,8 @@ async function cmdRestart(args) {
1392
2295
  }
1393
2296
 
1394
2297
  // src/mcp-config.ts
1395
- import fs2 from "fs/promises";
1396
- import path2 from "path";
2298
+ import fs4 from "fs/promises";
2299
+ import path4 from "path";
1397
2300
  import os2 from "os";
1398
2301
  import kleur8 from "kleur";
1399
2302
  var SERVER_NAME = "mimi-seed";
@@ -1465,12 +2368,12 @@ function removeTomlBlock(input, tableName) {
1465
2368
  return `${input.slice(0, start)}${input.slice(end).replace(/^\n+/, "\n")}`;
1466
2369
  }
1467
2370
  async function writeCodexMcpConfig(cfg) {
1468
- const configDir = path2.join(os2.homedir(), ".codex");
1469
- const configPath = path2.join(configDir, "config.toml");
1470
- await fs2.mkdir(configDir, { recursive: true });
2371
+ const configDir = path4.join(os2.homedir(), ".codex");
2372
+ const configPath = path4.join(configDir, "config.toml");
2373
+ await fs4.mkdir(configDir, { recursive: true });
1471
2374
  let current;
1472
2375
  try {
1473
- current = await fs2.readFile(configPath, "utf8");
2376
+ current = await fs4.readFile(configPath, "utf8");
1474
2377
  } catch {
1475
2378
  current = "";
1476
2379
  }
@@ -1480,14 +2383,14 @@ async function writeCodexMcpConfig(cfg) {
1480
2383
  current = removeTomlBlock(current, "mcp_servers.mimi-seed.http_headers");
1481
2384
  }
1482
2385
  const next = replaceTomlBlock(current, `mcp_servers.${CODEX_REMOTE_NAME}`, codexBlock(cfg));
1483
- await fs2.writeFile(configPath, next);
2386
+ await fs4.writeFile(configPath, next);
1484
2387
  return configPath;
1485
2388
  }
1486
2389
 
1487
2390
  // src/release-manifest.ts
1488
- import fs3 from "fs/promises";
1489
- import path3 from "path";
1490
- var RELEASE_MANIFEST_RELATIVE_PATH = path3.join("docs", "releases.json");
2391
+ import fs5 from "fs/promises";
2392
+ import path5 from "path";
2393
+ var RELEASE_MANIFEST_RELATIVE_PATH = path5.join("docs", "releases.json");
1491
2394
  var RELEASE_MANIFEST_TEMPLATE = {
1492
2395
  $schema: {
1493
2396
  _comment: "Mimi Seed release notes SSOT. Store notes, App Store What's New, in-app announcements, and app-version rollout metadata can share this file.",
@@ -1513,14 +2416,14 @@ var RELEASE_MANIFEST_TEMPLATE = {
1513
2416
  versions: {}
1514
2417
  };
1515
2418
  async function ensureReleaseManifest(cwd) {
1516
- const filePath = path3.join(cwd, RELEASE_MANIFEST_RELATIVE_PATH);
2419
+ const filePath = path5.join(cwd, RELEASE_MANIFEST_RELATIVE_PATH);
1517
2420
  try {
1518
- await fs3.access(filePath);
2421
+ await fs5.access(filePath);
1519
2422
  return { path: filePath, created: false };
1520
2423
  } catch {
1521
2424
  }
1522
- await fs3.mkdir(path3.dirname(filePath), { recursive: true });
1523
- await fs3.writeFile(filePath, `${JSON.stringify(RELEASE_MANIFEST_TEMPLATE, null, 2)}
2425
+ await fs5.mkdir(path5.dirname(filePath), { recursive: true });
2426
+ await fs5.writeFile(filePath, `${JSON.stringify(RELEASE_MANIFEST_TEMPLATE, null, 2)}
1524
2427
  `, {
1525
2428
  mode: 420
1526
2429
  });
@@ -1991,16 +2894,16 @@ async function cmdInit(args) {
1991
2894
  "- `/mimi-seed:health` \u2014 \uC5F0\uACB0 \uC0C1\uD0DC \uBE60\uB978 \uD655\uC778",
1992
2895
  "- `/mimi-seed:review-inbox` \u2014 \uBBF8\uB2F5\uBCC0 \uB9AC\uBDF0 \uB2F5\uBCC0"
1993
2896
  ].join("\n");
1994
- const claudeDir = path4.join(cwd, ".claude");
1995
- const claudeAgentPath = path4.join(claudeDir, "mimi-seed.md");
1996
- if (!fs4.existsSync(claudeAgentPath)) {
1997
- fs4.mkdirSync(claudeDir, { recursive: true });
1998
- fs4.writeFileSync(claudeAgentPath, agentMd, { mode: 420 });
2897
+ const claudeDir = path6.join(cwd, ".claude");
2898
+ const claudeAgentPath = path6.join(claudeDir, "mimi-seed.md");
2899
+ if (!fs6.existsSync(claudeAgentPath)) {
2900
+ fs6.mkdirSync(claudeDir, { recursive: true });
2901
+ fs6.writeFileSync(claudeAgentPath, agentMd, { mode: 420 });
1999
2902
  log4(kleur9.dim(M8().claudeAgent));
2000
2903
  }
2001
- const codexAgentPath = path4.join(cwd, "AGENTS.md");
2002
- if (!fs4.existsSync(codexAgentPath)) {
2003
- fs4.writeFileSync(codexAgentPath, agentMd, { mode: 420 });
2904
+ const codexAgentPath = path6.join(cwd, "AGENTS.md");
2905
+ if (!fs6.existsSync(codexAgentPath)) {
2906
+ fs6.writeFileSync(codexAgentPath, agentMd, { mode: 420 });
2004
2907
  log4(kleur9.dim(M8().codexAgent));
2005
2908
  }
2006
2909
  const manifest = await ensureReleaseManifest(cwd);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mimi-seed",
3
- "version": "0.19.8",
3
+ "version": "0.19.10",
4
4
  "description": "Mimi Seed CLI \u2014 Claude Code\uc640 Codex\uc5d0\uc11c \uc571 \ucd9c\uc2dc \uc6b4\uc601\uc744 \uad00\ub9ac\ud569\ub2c8\ub2e4.",
5
5
  "bin": {
6
6
  "mimi-seed": "dist/index.js"
@@ -18,9 +18,11 @@
18
18
  "LICENSE"
19
19
  ],
20
20
  "scripts": {
21
+ "prebuild": "node ../../scripts/sync-release-doctor.mjs --check",
21
22
  "build": "tsup",
22
23
  "dev": "tsx src/index.ts",
23
24
  "typecheck": "tsc --noEmit",
25
+ "pretest": "node ../../scripts/sync-release-doctor.mjs --check",
24
26
  "test": "npm run typecheck && npm run lint && vitest run",
25
27
  "test:watch": "vitest",
26
28
  "lint": "eslint .",