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.
- package/README.md +2 -1
- package/dist/index.js +953 -50
- 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
|
|
37
|
-
import
|
|
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
|
|
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
|
-
|
|
337
|
-
if (
|
|
338
|
-
|
|
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 (
|
|
342
|
-
if (
|
|
343
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
2067
|
+
import fs3 from "fs";
|
|
1165
2068
|
import os from "os";
|
|
1166
|
-
import
|
|
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(
|
|
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(
|
|
1268
|
-
const home = readJson(
|
|
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 =
|
|
2208
|
+
const exeBase = path3.basename(argv[0] ?? "");
|
|
1306
2209
|
const rest = argv.slice(1);
|
|
1307
2210
|
const hit = markers.some((marker) => {
|
|
1308
|
-
const base =
|
|
1309
|
-
return exeBase === base || rest.includes(marker) || rest.some((a) =>
|
|
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
|
|
1396
|
-
import
|
|
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 =
|
|
1469
|
-
const configPath =
|
|
1470
|
-
await
|
|
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
|
|
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
|
|
2386
|
+
await fs4.writeFile(configPath, next);
|
|
1484
2387
|
return configPath;
|
|
1485
2388
|
}
|
|
1486
2389
|
|
|
1487
2390
|
// src/release-manifest.ts
|
|
1488
|
-
import
|
|
1489
|
-
import
|
|
1490
|
-
var RELEASE_MANIFEST_RELATIVE_PATH =
|
|
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 =
|
|
2419
|
+
const filePath = path5.join(cwd, RELEASE_MANIFEST_RELATIVE_PATH);
|
|
1517
2420
|
try {
|
|
1518
|
-
await
|
|
2421
|
+
await fs5.access(filePath);
|
|
1519
2422
|
return { path: filePath, created: false };
|
|
1520
2423
|
} catch {
|
|
1521
2424
|
}
|
|
1522
|
-
await
|
|
1523
|
-
await
|
|
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 =
|
|
1995
|
-
const claudeAgentPath =
|
|
1996
|
-
if (!
|
|
1997
|
-
|
|
1998
|
-
|
|
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 =
|
|
2002
|
-
if (!
|
|
2003
|
-
|
|
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.
|
|
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 .",
|