mimi-seed 0.19.9 → 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 +907 -40
- 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,6 +260,868 @@ 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",
|
|
@@ -267,7 +1129,8 @@ var M2 = catalog(
|
|
|
267
1129
|
unknownOption: (option) => `\uC54C \uC218 \uC5C6\uB294 check \uC635\uC158: ${option}`,
|
|
268
1130
|
unexpectedArgument: (value) => `\uC608\uC0C1\uD558\uC9C0 \uC54A\uC740 check \uC778\uC790: ${value}`,
|
|
269
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",
|
|
270
|
-
|
|
1132
|
+
localFailed: (message) => `Release Doctor \uC2E4\uD589 \uC2E4\uD328: ${message}
|
|
1133
|
+
`,
|
|
271
1134
|
title: "mimi-seed check \u2014 \uCD9C\uC2DC \uC804 \uC810\uAC80\n\n",
|
|
272
1135
|
appsFailed: (msg) => `\uC571 \uBAA9\uB85D \uC870\uD68C \uC2E4\uD328: ${msg}
|
|
273
1136
|
`,
|
|
@@ -303,7 +1166,8 @@ var M2 = catalog(
|
|
|
303
1166
|
unknownOption: (option) => `Unknown check option: ${option}`,
|
|
304
1167
|
unexpectedArgument: (value) => `Unexpected check argument: ${value}`,
|
|
305
1168
|
localAppConflict: "--app is remote-only and cannot be combined with --local, --path, or --json.\n",
|
|
306
|
-
|
|
1169
|
+
localFailed: (message) => `Release Doctor failed: ${message}
|
|
1170
|
+
`,
|
|
307
1171
|
title: "mimi-seed check \u2014 pre-launch check\n\n",
|
|
308
1172
|
appsFailed: (msg) => `Failed to list apps: ${msg}
|
|
309
1173
|
`,
|
|
@@ -396,12 +1260,15 @@ async function cmdCheck(argv) {
|
|
|
396
1260
|
return;
|
|
397
1261
|
}
|
|
398
1262
|
if (localRequested || !cfg) {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
+
}
|
|
405
1272
|
return;
|
|
406
1273
|
}
|
|
407
1274
|
process.stdout.write(kleur2.bold(M2().title));
|
|
@@ -1197,9 +2064,9 @@ async function cmdGa4(args) {
|
|
|
1197
2064
|
|
|
1198
2065
|
// src/mcp-restart.ts
|
|
1199
2066
|
import { execSync as execSync2 } from "child_process";
|
|
1200
|
-
import
|
|
2067
|
+
import fs3 from "fs";
|
|
1201
2068
|
import os from "os";
|
|
1202
|
-
import
|
|
2069
|
+
import path3 from "path";
|
|
1203
2070
|
import kleur7 from "kleur";
|
|
1204
2071
|
function log3(msg) {
|
|
1205
2072
|
process.stdout.write(msg + "\n");
|
|
@@ -1260,7 +2127,7 @@ var M6 = catalog(
|
|
|
1260
2127
|
);
|
|
1261
2128
|
function readJson(filePath) {
|
|
1262
2129
|
try {
|
|
1263
|
-
return JSON.parse(
|
|
2130
|
+
return JSON.parse(fs3.readFileSync(filePath, "utf8"));
|
|
1264
2131
|
} catch {
|
|
1265
2132
|
return {};
|
|
1266
2133
|
}
|
|
@@ -1300,8 +2167,8 @@ function collectServers() {
|
|
|
1300
2167
|
for (const [name, cfg] of entries) if (!(name in servers)) servers[name] = cfg;
|
|
1301
2168
|
};
|
|
1302
2169
|
const projectDir = process.cwd();
|
|
1303
|
-
add(readJson(
|
|
1304
|
-
const home = readJson(
|
|
2170
|
+
add(readJson(path3.join(projectDir, ".mcp.json")).mcpServers, ".mcp.json");
|
|
2171
|
+
const home = readJson(path3.join(os.homedir(), ".claude.json"));
|
|
1305
2172
|
const projects = home.projects;
|
|
1306
2173
|
add(projects?.[projectDir]?.mcpServers, "~/.claude.json (projects)");
|
|
1307
2174
|
add(home.mcpServers, "~/.claude.json");
|
|
@@ -1338,11 +2205,11 @@ function findPids(markers) {
|
|
|
1338
2205
|
const pid = Number(m[1]);
|
|
1339
2206
|
if (skip.has(pid)) continue;
|
|
1340
2207
|
const argv = m[2].split(/\s+/).filter(Boolean);
|
|
1341
|
-
const exeBase =
|
|
2208
|
+
const exeBase = path3.basename(argv[0] ?? "");
|
|
1342
2209
|
const rest = argv.slice(1);
|
|
1343
2210
|
const hit = markers.some((marker) => {
|
|
1344
|
-
const base =
|
|
1345
|
-
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);
|
|
1346
2213
|
});
|
|
1347
2214
|
if (hit) pids.push(pid);
|
|
1348
2215
|
}
|
|
@@ -1428,8 +2295,8 @@ async function cmdRestart(args) {
|
|
|
1428
2295
|
}
|
|
1429
2296
|
|
|
1430
2297
|
// src/mcp-config.ts
|
|
1431
|
-
import
|
|
1432
|
-
import
|
|
2298
|
+
import fs4 from "fs/promises";
|
|
2299
|
+
import path4 from "path";
|
|
1433
2300
|
import os2 from "os";
|
|
1434
2301
|
import kleur8 from "kleur";
|
|
1435
2302
|
var SERVER_NAME = "mimi-seed";
|
|
@@ -1501,12 +2368,12 @@ function removeTomlBlock(input, tableName) {
|
|
|
1501
2368
|
return `${input.slice(0, start)}${input.slice(end).replace(/^\n+/, "\n")}`;
|
|
1502
2369
|
}
|
|
1503
2370
|
async function writeCodexMcpConfig(cfg) {
|
|
1504
|
-
const configDir =
|
|
1505
|
-
const configPath =
|
|
1506
|
-
await
|
|
2371
|
+
const configDir = path4.join(os2.homedir(), ".codex");
|
|
2372
|
+
const configPath = path4.join(configDir, "config.toml");
|
|
2373
|
+
await fs4.mkdir(configDir, { recursive: true });
|
|
1507
2374
|
let current;
|
|
1508
2375
|
try {
|
|
1509
|
-
current = await
|
|
2376
|
+
current = await fs4.readFile(configPath, "utf8");
|
|
1510
2377
|
} catch {
|
|
1511
2378
|
current = "";
|
|
1512
2379
|
}
|
|
@@ -1516,14 +2383,14 @@ async function writeCodexMcpConfig(cfg) {
|
|
|
1516
2383
|
current = removeTomlBlock(current, "mcp_servers.mimi-seed.http_headers");
|
|
1517
2384
|
}
|
|
1518
2385
|
const next = replaceTomlBlock(current, `mcp_servers.${CODEX_REMOTE_NAME}`, codexBlock(cfg));
|
|
1519
|
-
await
|
|
2386
|
+
await fs4.writeFile(configPath, next);
|
|
1520
2387
|
return configPath;
|
|
1521
2388
|
}
|
|
1522
2389
|
|
|
1523
2390
|
// src/release-manifest.ts
|
|
1524
|
-
import
|
|
1525
|
-
import
|
|
1526
|
-
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");
|
|
1527
2394
|
var RELEASE_MANIFEST_TEMPLATE = {
|
|
1528
2395
|
$schema: {
|
|
1529
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.",
|
|
@@ -1549,14 +2416,14 @@ var RELEASE_MANIFEST_TEMPLATE = {
|
|
|
1549
2416
|
versions: {}
|
|
1550
2417
|
};
|
|
1551
2418
|
async function ensureReleaseManifest(cwd) {
|
|
1552
|
-
const filePath =
|
|
2419
|
+
const filePath = path5.join(cwd, RELEASE_MANIFEST_RELATIVE_PATH);
|
|
1553
2420
|
try {
|
|
1554
|
-
await
|
|
2421
|
+
await fs5.access(filePath);
|
|
1555
2422
|
return { path: filePath, created: false };
|
|
1556
2423
|
} catch {
|
|
1557
2424
|
}
|
|
1558
|
-
await
|
|
1559
|
-
await
|
|
2425
|
+
await fs5.mkdir(path5.dirname(filePath), { recursive: true });
|
|
2426
|
+
await fs5.writeFile(filePath, `${JSON.stringify(RELEASE_MANIFEST_TEMPLATE, null, 2)}
|
|
1560
2427
|
`, {
|
|
1561
2428
|
mode: 420
|
|
1562
2429
|
});
|
|
@@ -2027,16 +2894,16 @@ async function cmdInit(args) {
|
|
|
2027
2894
|
"- `/mimi-seed:health` \u2014 \uC5F0\uACB0 \uC0C1\uD0DC \uBE60\uB978 \uD655\uC778",
|
|
2028
2895
|
"- `/mimi-seed:review-inbox` \u2014 \uBBF8\uB2F5\uBCC0 \uB9AC\uBDF0 \uB2F5\uBCC0"
|
|
2029
2896
|
].join("\n");
|
|
2030
|
-
const claudeDir =
|
|
2031
|
-
const claudeAgentPath =
|
|
2032
|
-
if (!
|
|
2033
|
-
|
|
2034
|
-
|
|
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 });
|
|
2035
2902
|
log4(kleur9.dim(M8().claudeAgent));
|
|
2036
2903
|
}
|
|
2037
|
-
const codexAgentPath =
|
|
2038
|
-
if (!
|
|
2039
|
-
|
|
2904
|
+
const codexAgentPath = path6.join(cwd, "AGENTS.md");
|
|
2905
|
+
if (!fs6.existsSync(codexAgentPath)) {
|
|
2906
|
+
fs6.writeFileSync(codexAgentPath, agentMd, { mode: 420 });
|
|
2040
2907
|
log4(kleur9.dim(M8().codexAgent));
|
|
2041
2908
|
}
|
|
2042
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 .",
|