@leonxin/meetgames 0.1.8 → 0.1.12
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/.agents/skills/meet-sdk-regression/SKILL.md +93 -0
- package/.cursor/mcp.example.json +16 -0
- package/.cursor/mcp.json +11 -0
- package/.cursor/skills/meetgames-mcp/SKILL.md +48 -0
- package/.vite/vitest/results.json +1 -0
- package/README.md +36 -13
- package/{meetsdk-android.json → config/meetsdk-android.json} +2 -1
- package/config/meetsdk-ios.json +15 -0
- package/dist/android/adapter.d.ts.map +1 -1
- package/dist/android/adapter.js +2 -2
- package/dist/android/adapter.js.map +1 -1
- package/dist/android/detect.d.ts +2 -2
- package/dist/android/detect.d.ts.map +1 -1
- package/dist/android/detect.js +36 -8
- package/dist/android/detect.js.map +1 -1
- package/dist/cache.d.ts +44 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +101 -0
- package/dist/cache.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +181 -49
- package/dist/cli.js.map +1 -1
- package/dist/config/meetSdkDefaultConfig.d.ts +19 -2
- package/dist/config/meetSdkDefaultConfig.d.ts.map +1 -1
- package/dist/config/meetSdkDefaultConfig.js +69 -6
- package/dist/config/meetSdkDefaultConfig.js.map +1 -1
- package/dist/config/meetSdkIosConfig.d.ts +21 -0
- package/dist/config/meetSdkIosConfig.d.ts.map +1 -0
- package/dist/config/meetSdkIosConfig.js +68 -0
- package/dist/config/meetSdkIosConfig.js.map +1 -0
- package/dist/config/meetSdkRemoteConfig.d.ts +19 -1
- package/dist/config/meetSdkRemoteConfig.d.ts.map +1 -1
- package/dist/config/meetSdkRemoteConfig.js +64 -25
- package/dist/config/meetSdkRemoteConfig.js.map +1 -1
- package/dist/contracts/types.d.ts +27 -6
- package/dist/contracts/types.d.ts.map +1 -1
- package/dist/core/doctor.d.ts +17 -0
- package/dist/core/doctor.d.ts.map +1 -0
- package/dist/core/doctor.js +444 -0
- package/dist/core/doctor.js.map +1 -0
- package/dist/core/pipeline.d.ts.map +1 -1
- package/dist/core/pipeline.js +0 -15
- package/dist/core/pipeline.js.map +1 -1
- package/dist/core/platform.d.ts +12 -0
- package/dist/core/platform.d.ts.map +1 -0
- package/dist/core/platform.js +40 -0
- package/dist/core/platform.js.map +1 -0
- package/dist/core/previewPatches.d.ts +1 -1
- package/dist/core/previewPatches.js +2 -2
- package/dist/core/previewPatches.js.map +1 -1
- package/dist/core/reporter.js +1 -1
- package/dist/core/reporter.js.map +1 -1
- package/dist/core/workspace.d.ts +2 -2
- package/dist/core/workspace.d.ts.map +1 -1
- package/dist/core/workspace.js +6 -5
- package/dist/core/workspace.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/ios/channelConfig.d.ts +1 -0
- package/dist/ios/channelConfig.d.ts.map +1 -1
- package/dist/ios/channelConfig.js +82 -0
- package/dist/ios/channelConfig.js.map +1 -1
- package/dist/ios/codeUtils.d.ts +1 -0
- package/dist/ios/codeUtils.d.ts.map +1 -1
- package/dist/ios/codeUtils.js +11 -2
- package/dist/ios/codeUtils.js.map +1 -1
- package/dist/ios/detect.d.ts +2 -2
- package/dist/ios/detect.d.ts.map +1 -1
- package/dist/ios/detect.js +49 -10
- package/dist/ios/detect.js.map +1 -1
- package/dist/ios/entitlements.d.ts +4 -0
- package/dist/ios/entitlements.d.ts.map +1 -0
- package/dist/ios/entitlements.js +53 -0
- package/dist/ios/entitlements.js.map +1 -0
- package/dist/ios/fileManager.d.ts.map +1 -1
- package/dist/ios/fileManager.js +3 -2
- package/dist/ios/fileManager.js.map +1 -1
- package/dist/ios/infoPlist.d.ts +1 -1
- package/dist/ios/infoPlist.d.ts.map +1 -1
- package/dist/ios/infoPlist.js.map +1 -1
- package/dist/ios/integrate.d.ts.map +1 -1
- package/dist/ios/integrate.js +214 -39
- package/dist/ios/integrate.js.map +1 -1
- package/dist/ios/pbxprojEditor.d.ts +2 -0
- package/dist/ios/pbxprojEditor.d.ts.map +1 -1
- package/dist/ios/pbxprojEditor.js +250 -6
- package/dist/ios/pbxprojEditor.js.map +1 -1
- package/dist/ios/pluginConfig.d.ts +1 -0
- package/dist/ios/pluginConfig.d.ts.map +1 -1
- package/dist/ios/pluginConfig.js +36 -4
- package/dist/ios/pluginConfig.js.map +1 -1
- package/dist/ios/sdkBundle.d.ts +1 -6
- package/dist/ios/sdkBundle.d.ts.map +1 -1
- package/dist/ios/sdkBundle.js +47 -17
- package/dist/ios/sdkBundle.js.map +1 -1
- package/dist/ios/template.d.ts +1 -0
- package/dist/ios/template.d.ts.map +1 -1
- package/dist/ios/template.js +14 -1
- package/dist/ios/template.js.map +1 -1
- package/dist/ios/types.d.ts +2 -2
- package/dist/ios/types.d.ts.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +22 -15
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/service.d.ts +11 -6
- package/dist/mcp/service.d.ts.map +1 -1
- package/dist/mcp/service.js +61 -18
- package/dist/mcp/service.js.map +1 -1
- package/dist/ops/handlers.d.ts.map +1 -1
- package/dist/ops/handlers.js +34 -23
- package/dist/ops/handlers.js.map +1 -1
- package/dist/remote/sdkHomeDownload.d.ts +65 -0
- package/dist/remote/sdkHomeDownload.d.ts.map +1 -0
- package/dist/remote/sdkHomeDownload.js +229 -0
- package/dist/remote/sdkHomeDownload.js.map +1 -0
- package/dist/remote/topsdkDownloadSdkConfig.d.ts.map +1 -1
- package/dist/remote/topsdkDownloadSdkConfig.js +11 -1
- package/dist/remote/topsdkDownloadSdkConfig.js.map +1 -1
- package/dist/shared/errors.d.ts +7 -0
- package/dist/shared/errors.d.ts.map +1 -0
- package/dist/shared/errors.js +16 -0
- package/dist/shared/errors.js.map +1 -0
- package/docs/API.md +246 -32
- package/docs/CLI.md +291 -0
- package/docs/INTEGRATION.md +116 -0
- package/docs/MCP.md +86 -0
- package/docs/README.md +18 -10
- package/docs/{api → archive/api}/downloadSDKConfig.md +9 -7
- package/docs/{api → archive/api}/getChannelConfig-meetgames.md +2 -2
- package/docs/archive/ios-migration.md +2139 -0
- package/docs/{ → archive/product/}/346/212/200/346/234/257/346/226/271/346/241/210/350/260/203/347/240/224.md +7 -7
- package/docs/{ → archive/product/}/351/234/200/346/261/202/346/226/207/346/241/243.md +16 -15
- package/package.json +7 -36
- package/recipes/android-default.yaml +0 -5
- package/recipes/integrate-default.yaml +0 -5
- package/src/android/adapter.ts +9 -0
- package/src/android/assembleIntegrationJson.ts +33 -0
- package/src/android/detect.ts +132 -0
- package/src/android/downloadGoogleServicesJson.ts +56 -0
- package/src/android/gradle.ts +116 -0
- package/src/android/manifest.ts +50 -0
- package/src/android/meetSdkRemoteGradle.ts +837 -0
- package/src/cache.ts +164 -0
- package/src/cli.ts +496 -0
- package/src/config/fetchConfigWrite.ts +30 -0
- package/src/config/loadAndroidIntegration.ts +41 -0
- package/src/config/loadManifest.ts +40 -0
- package/src/config/meetSdkDefaultConfig.ts +100 -0
- package/src/config/meetSdkIosConfig.ts +89 -0
- package/src/config/meetSdkRemoteConfig.ts +1215 -0
- package/src/config/topsdkFeatureModules.ts +92 -0
- package/src/contracts/types.ts +129 -0
- package/src/core/doctor.ts +485 -0
- package/src/core/patch.ts +64 -0
- package/src/core/pipeline.ts +107 -0
- package/src/core/platform.ts +47 -0
- package/src/core/previewPatches.ts +23 -0
- package/src/core/reporter.ts +24 -0
- package/src/core/workspace.ts +31 -0
- package/src/entry.ts +7 -0
- package/src/index.ts +140 -0
- package/src/ios/channelConfig.ts +128 -0
- package/src/ios/codeUtils.ts +160 -0
- package/src/ios/detect.ts +105 -0
- package/src/ios/entitlements.ts +61 -0
- package/src/ios/fileManager.ts +48 -0
- package/src/ios/infoPlist.ts +55 -0
- package/src/ios/integrate.ts +517 -0
- package/src/ios/pbxprojEditor.ts +450 -0
- package/src/ios/pluginConfig.ts +97 -0
- package/src/ios/reserved.ts +8 -0
- package/src/ios/sdkBundle.ts +59 -0
- package/src/ios/template.ts +36 -0
- package/src/ios/types.ts +65 -0
- package/src/mcp/server.ts +176 -0
- package/src/mcp/service.ts +253 -0
- package/src/mcp-entry.ts +7 -0
- package/src/ops/fileStore.ts +56 -0
- package/src/ops/handlers.ts +308 -0
- package/src/remote/fetchJson.ts +22 -0
- package/src/remote/sdkHomeDownload.ts +295 -0
- package/src/remote/topsdkDownloadSdkConfig.ts +93 -0
- package/src/remote/topsdkGetSdkConfig.ts +122 -0
- package/src/remote/topsdkSign.ts +10 -0
- package/src/shared/errors.ts +16 -0
- package/tests/assemble.test.ts +12 -0
- package/tests/doctor.test.ts +91 -0
- package/tests/downloadGoogleServicesJson.test.ts +47 -0
- package/tests/fetch-remote.test.ts +23 -0
- package/tests/fetchConfigOverrides.test.ts +28 -0
- package/tests/fetchConfigWrite.test.ts +54 -0
- package/tests/fixtures-hosts.test.ts +78 -0
- package/tests/gradle.test.ts +33 -0
- package/tests/integration-json.test.ts +29 -0
- package/tests/ios.codeUtils.test.ts +23 -0
- package/tests/ios.sdkBundle.test.ts +21 -0
- package/tests/loadManifest.test.ts +15 -0
- package/tests/manifest-xml.test.ts +30 -0
- package/tests/mcp.e2e.ts +214 -0
- package/tests/mcp.service.test.ts +53 -0
- package/tests/meetSdkRemoteConfig.test.ts +481 -0
- package/tests/meetSdkRemoteGradle.test.ts +414 -0
- package/tests/pipeline.android.test.ts +95 -0
- package/tests/pipeline.integration-json.test.ts +58 -0
- package/tests/pipeline.ios.test.ts +392 -0
- package/tests/pipeline.preview.patch.test.ts +85 -0
- package/tests/platformSelection.test.ts +77 -0
- package/tests/sdkHomeDownload.test.ts +124 -0
- package/tests/sdkVersionConfig.test.ts +131 -0
- package/tests/topsdk.test.ts +53 -0
- package/tests/topsdkDownloadSdkConfig.test.ts +81 -0
- package/tests/topsdkFeatureModules.test.ts +116 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +9 -0
- package/vitest.mcp.config.ts +11 -0
- package/bundled/android/sample.txt +0 -1
- package/docs/ANDROID.md +0 -133
- package/docs/CURSOR-MCP-SETUP.md +0 -72
- package/docs/MCP-SKILL.md +0 -63
- package/fixtures/api-samples/getChannelConfig-meetgames.sample.json +0 -123
- package/fixtures/meetsdk-remote-config.download-shape.json +0 -20
- package/fixtures/meetsdk-remote-config.mock.json +0 -69
- package/fixtures/recipes/android-default.fixture.yaml +0 -15
- package/fixtures/recipes/android-integration.fixture.json +0 -29
- package/fixtures/topsdk-config-reference.json +0 -39
- /package/docs/{api → archive/api}/getSDKConfig.md +0 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
applicationModuleRelPath,
|
|
5
|
+
} from "../android/detect.js";
|
|
6
|
+
import {
|
|
7
|
+
detectModuleGradlePluginStyle,
|
|
8
|
+
extractImplementationKey,
|
|
9
|
+
extractResValueKey,
|
|
10
|
+
mavenGroupArtifactKey,
|
|
11
|
+
} from "../android/meetSdkRemoteGradle.js";
|
|
12
|
+
import {
|
|
13
|
+
MEETSDK_REMOTE_CONFIG_FILENAME,
|
|
14
|
+
applyMeetSdkDefaultConfig,
|
|
15
|
+
collectMeetSdkRemoteBuildscriptClasspaths,
|
|
16
|
+
collectMeetSdkRemoteBuildscriptRepositories,
|
|
17
|
+
collectMeetSdkRemotePluginsDslForModule,
|
|
18
|
+
collectMeetSdkRemotePluginsDslForRoot,
|
|
19
|
+
collectMeetSdkRemoteRepositories,
|
|
20
|
+
hasSdkModuleKey,
|
|
21
|
+
tryParseAsMeetSdkRemoteConfig,
|
|
22
|
+
validateMeetSdkRemoteConfig,
|
|
23
|
+
type MeetSdkRemoteConfig,
|
|
24
|
+
} from "../config/meetSdkRemoteConfig.js";
|
|
25
|
+
import { loadMeetSdkDefaultConfigWithLatestAndroidVersion } from "../config/meetSdkDefaultConfig.js";
|
|
26
|
+
import {
|
|
27
|
+
TOPSDK_FEATURE_MODULES,
|
|
28
|
+
} from "../config/topsdkFeatureModules.js";
|
|
29
|
+
import type { WorkspaceContext } from "../contracts/types.js";
|
|
30
|
+
import { buildChannelConfigMap, enabledIosPluginFolders, unsupportedIosModuleKeys } from "../ios/channelConfig.js";
|
|
31
|
+
import { findDelegateFiles, findSceneDelegateFiles } from "../ios/codeUtils.js";
|
|
32
|
+
import { parsePlistXml } from "../ios/infoPlist.js";
|
|
33
|
+
import { findInfoPlistPathsFromPbx, loadPbxFromStore } from "../ios/pbxprojEditor.js";
|
|
34
|
+
import { findPluginByFolder, listSdkCoreConfigs, validateLoadedPluginResources } from "../ios/pluginConfig.js";
|
|
35
|
+
import { resolveIosSdkRoot } from "../ios/sdkBundle.js";
|
|
36
|
+
import { applyChannelTemplate, applyChannelTemplateValue } from "../ios/template.js";
|
|
37
|
+
import type { CodeConfig, LoadedPluginConfig } from "../ios/types.js";
|
|
38
|
+
import { TextFileStore } from "../ops/fileStore.js";
|
|
39
|
+
|
|
40
|
+
export interface DoctorCheck {
|
|
41
|
+
name: string;
|
|
42
|
+
ok: boolean;
|
|
43
|
+
details?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface DoctorReport {
|
|
47
|
+
projectRoot: string;
|
|
48
|
+
platform: "android" | "ios";
|
|
49
|
+
configPath: string;
|
|
50
|
+
ok: boolean;
|
|
51
|
+
checks: DoctorCheck[];
|
|
52
|
+
warnings: string[];
|
|
53
|
+
errors: string[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function addCheck(report: DoctorReport, name: string, ok: boolean, details?: string): void {
|
|
57
|
+
report.checks.push({ name, ok, ...(details ? { details } : {}) });
|
|
58
|
+
if (!ok) report.errors.push(details ? `${name}: ${details}` : name);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readJsonConfig(configPath: string): MeetSdkRemoteConfig | null {
|
|
62
|
+
const abs = configPath;
|
|
63
|
+
if (!fs.existsSync(abs)) return null;
|
|
64
|
+
try {
|
|
65
|
+
return tryParseAsMeetSdkRemoteConfig(JSON.parse(fs.readFileSync(abs, "utf8")) as unknown);
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readFileIfExists(abs: string): string {
|
|
72
|
+
return fs.existsSync(abs) ? fs.readFileSync(abs, "utf8") : "";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function lineDeclaresRepository(line: string, repo: string): boolean {
|
|
76
|
+
const t = line.trim();
|
|
77
|
+
if (repo === "google") return /\bgoogle\s*\(\s*\)/.test(t);
|
|
78
|
+
if (repo === "mavenCentral") return /\bmavenCentral\s*\(\s*\)/.test(t);
|
|
79
|
+
return t.includes("maven") && t.includes(repo);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function fileContainsRepository(text: string, repo: string): boolean {
|
|
83
|
+
return text.split("\n").some((line) => lineDeclaresRepository(line, repo));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function moduleBuildGradlePath(ctx: WorkspaceContext): string | null {
|
|
87
|
+
if (!ctx.android?.ok) return null;
|
|
88
|
+
const groovy = path.join(ctx.android.moduleDir, "build.gradle");
|
|
89
|
+
if (fs.existsSync(groovy)) return groovy;
|
|
90
|
+
const kts = path.join(ctx.android.moduleDir, "build.gradle.kts");
|
|
91
|
+
return fs.existsSync(kts) ? kts : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function rootBuildGradlePath(projectRoot: string): string | null {
|
|
95
|
+
for (const name of ["build.gradle", "build.gradle.kts"]) {
|
|
96
|
+
const abs = path.join(projectRoot, name);
|
|
97
|
+
if (fs.existsSync(abs)) return abs;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function settingsGradlePath(projectRoot: string): string | null {
|
|
103
|
+
for (const name of ["settings.gradle", "settings.gradle.kts"]) {
|
|
104
|
+
const abs = path.join(projectRoot, name);
|
|
105
|
+
if (fs.existsSync(abs)) return abs;
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function extractResValues(text: string): Map<string, string> {
|
|
111
|
+
const out = new Map<string, string>();
|
|
112
|
+
for (const line of text.split("\n")) {
|
|
113
|
+
const key = extractResValueKey(line);
|
|
114
|
+
if (!key) continue;
|
|
115
|
+
const m = line.match(/resValue\s*\(\s*['"][^'"]+['"]\s*,\s*['"][^'"]+['"]\s*,\s*(.*?)\s*\)\s*$/);
|
|
116
|
+
if (!m) continue;
|
|
117
|
+
out.set(key, m[1].replace(/^['"]|['"]$/g, ""));
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function putExpected(out: Map<string, string>, key: string, value: unknown): void {
|
|
123
|
+
if (value === undefined || value === null || String(value).length === 0) return;
|
|
124
|
+
out.set(key, String(value));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function expectedAndroidResValues(config: MeetSdkRemoteConfig): Map<string, string> {
|
|
128
|
+
const out = new Map<string, string>();
|
|
129
|
+
putExpected(out, "top_channel_id", config.channel);
|
|
130
|
+
putExpected(out, "top_app_id", config.topsdk.appId);
|
|
131
|
+
const L = config.sdkModules.login;
|
|
132
|
+
const A = config.sdkModules.analytics;
|
|
133
|
+
if (typeof L.facebook === "object" && L.facebook) {
|
|
134
|
+
putExpected(out, "facebook_app_id", L.facebook.facebookAppId);
|
|
135
|
+
putExpected(out, "fb_login_protocol_scheme", L.facebook.fbLoginProtocolScheme);
|
|
136
|
+
putExpected(out, "facebook_client_token", L.facebook.facebookClientToken);
|
|
137
|
+
}
|
|
138
|
+
if (typeof L.google === "object" && L.google) putExpected(out, "google_client_id", L.google.googleClientId);
|
|
139
|
+
if (typeof L.twitter === "object" && L.twitter) {
|
|
140
|
+
putExpected(out, "twitter_client_id", L.twitter.clientId);
|
|
141
|
+
putExpected(out, "twitter_client_secret", L.twitter.secret);
|
|
142
|
+
putExpected(out, "twitter_redirect_url", L.twitter.redirect);
|
|
143
|
+
}
|
|
144
|
+
if (typeof L.snapchat === "object" && L.snapchat) {
|
|
145
|
+
putExpected(out, "snapchat_client_id", L.snapchat.clientId);
|
|
146
|
+
putExpected(out, "snapchat_redirect_uri", L.snapchat.redirect);
|
|
147
|
+
}
|
|
148
|
+
if (typeof L.tiktok === "object" && L.tiktok) {
|
|
149
|
+
putExpected(out, "tiktok_client_id", L.tiktok.clientId);
|
|
150
|
+
putExpected(out, "tiktok_client_secret", L.tiktok.secret);
|
|
151
|
+
putExpected(out, "tiktok_redirect_uri", L.tiktok.redirect);
|
|
152
|
+
}
|
|
153
|
+
if (typeof L.discord === "object" && L.discord) {
|
|
154
|
+
putExpected(out, "discord_client_id", L.discord.clientId);
|
|
155
|
+
putExpected(out, "discord_client_secret", L.discord.secret);
|
|
156
|
+
putExpected(out, "discord_redirect_uri", L.discord.redirect);
|
|
157
|
+
}
|
|
158
|
+
if (typeof L.line === "object" && L.line) putExpected(out, "line_channel_id", L.line.clientId);
|
|
159
|
+
if (typeof L.kakao === "object" && L.kakao) {
|
|
160
|
+
putExpected(out, "kakao_app_id", L.kakao.clientId);
|
|
161
|
+
putExpected(out, "kakao_scheme", L.kakao.scheme);
|
|
162
|
+
}
|
|
163
|
+
if (typeof L.naver === "object" && L.naver) {
|
|
164
|
+
putExpected(out, "naver_client_id", L.naver.clientId);
|
|
165
|
+
putExpected(out, "naver_client_secret", L.naver.secret);
|
|
166
|
+
putExpected(out, "naver_client_name", L.naver.name);
|
|
167
|
+
}
|
|
168
|
+
if (typeof A.appsflyer === "object" && A.appsflyer) {
|
|
169
|
+
putExpected(out, "appsflyer_enable_debug_log", Boolean(A.appsflyer.enableDebugLog));
|
|
170
|
+
putExpected(out, "af_dev_key", A.appsflyer.devKey);
|
|
171
|
+
}
|
|
172
|
+
if (typeof A.adjust === "object" && A.adjust) {
|
|
173
|
+
putExpected(out, "adjust_enable_sandbox", Boolean(A.adjust.enableSandbox));
|
|
174
|
+
putExpected(out, "adjust_app_token", A.adjust.appId);
|
|
175
|
+
}
|
|
176
|
+
if (typeof A.facebookdata === "object" && A.facebookdata) {
|
|
177
|
+
putExpected(out, "facebook_data_app_id", A.facebookdata.facebookAppId);
|
|
178
|
+
putExpected(out, "facebook_data_client_token", A.facebookdata.facebookClientToken);
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function checkAndroidDependency(moduleGradle: string, groupId: string, artifact: string): boolean {
|
|
184
|
+
for (const line of moduleGradle.split("\n")) {
|
|
185
|
+
const key = extractImplementationKey(line, moduleGradle, groupId);
|
|
186
|
+
if (key === `${groupId}:${artifact}`) return true;
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function checkAndroid(ctx: WorkspaceContext, report: DoctorReport, config: MeetSdkRemoteConfig): Promise<void> {
|
|
192
|
+
if (!ctx.android?.ok) {
|
|
193
|
+
addCheck(report, "android.detect", false, "android application module not detected");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const resolved = applyMeetSdkDefaultConfig(
|
|
197
|
+
config,
|
|
198
|
+
await loadMeetSdkDefaultConfigWithLatestAndroidVersion({
|
|
199
|
+
sdkHomeApiBaseUrl: ctx.sdkHomeApiBaseUrl,
|
|
200
|
+
packageRoot: ctx.packageRoot,
|
|
201
|
+
})
|
|
202
|
+
);
|
|
203
|
+
const moduleGradleAbs = moduleBuildGradlePath(ctx);
|
|
204
|
+
const rootGradleAbs = rootBuildGradlePath(ctx.projectRoot);
|
|
205
|
+
const settingsGradleAbs = settingsGradlePath(ctx.projectRoot);
|
|
206
|
+
const moduleGradle = moduleGradleAbs ? readFileIfExists(moduleGradleAbs) : "";
|
|
207
|
+
const rootGradle = rootGradleAbs ? readFileIfExists(rootGradleAbs) : "";
|
|
208
|
+
const settingsGradle = settingsGradleAbs ? readFileIfExists(settingsGradleAbs) : "";
|
|
209
|
+
|
|
210
|
+
addCheck(report, "android.moduleGradle.exists", Boolean(moduleGradleAbs), moduleGradleAbs ?? "module build.gradle not found");
|
|
211
|
+
addCheck(report, "android.applicationId", moduleGradle.includes(`applicationId "${resolved.packageName}"`) || moduleGradle.includes(`applicationId '${resolved.packageName}'`), resolved.packageName);
|
|
212
|
+
|
|
213
|
+
const repoText = `${settingsGradle}\n${rootGradle}`;
|
|
214
|
+
const missingRepos = collectMeetSdkRemoteRepositories(resolved).filter((repo) => !fileContainsRepository(repoText, repo));
|
|
215
|
+
addCheck(report, "android.repositories", missingRepos.length === 0, missingRepos.length ? `missing: ${missingRepos.join(", ")}` : "all expected repositories found");
|
|
216
|
+
|
|
217
|
+
const style = moduleGradle ? detectModuleGradlePluginStyle(moduleGradle) : "apply-plugin";
|
|
218
|
+
const rootPluginIds = collectMeetSdkRemotePluginsDslForRoot(resolved).map((p) => p.id);
|
|
219
|
+
const modulePluginIds = collectMeetSdkRemotePluginsDslForModule(resolved).map((p) => p.id);
|
|
220
|
+
if (style === "plugins-dsl") {
|
|
221
|
+
const missingRootPlugins = rootPluginIds.filter((id) => !rootGradle.includes(id));
|
|
222
|
+
const missingModulePlugins = modulePluginIds.filter((id) => !moduleGradle.includes(id));
|
|
223
|
+
addCheck(report, "android.gradlePlugins", missingRootPlugins.length + missingModulePlugins.length === 0, [...missingRootPlugins, ...missingModulePlugins].join(", ") || "all expected plugins found");
|
|
224
|
+
} else {
|
|
225
|
+
const missingClasspaths = collectMeetSdkRemoteBuildscriptClasspaths(resolved).filter((cp) => !rootGradle.includes(cp));
|
|
226
|
+
const missingBuildscriptRepos = collectMeetSdkRemoteBuildscriptRepositories(resolved).filter((repo) => !fileContainsRepository(rootGradle, repo));
|
|
227
|
+
addCheck(report, "android.buildscript", missingClasspaths.length + missingBuildscriptRepos.length === 0, [...missingClasspaths, ...missingBuildscriptRepos].join(", ") || "buildscript is configured");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const expectedArtifacts = ["ui"];
|
|
231
|
+
for (const m of TOPSDK_FEATURE_MODULES) {
|
|
232
|
+
const bucket = resolved.sdkModules[m.scope] as unknown as Record<string, unknown>;
|
|
233
|
+
if (hasSdkModuleKey(bucket, m.subKey)) expectedArtifacts.push(m.topsdkArtifact);
|
|
234
|
+
}
|
|
235
|
+
const missingDeps = expectedArtifacts.filter((artifact) => !checkAndroidDependency(moduleGradle, resolved.topsdk.groupId, artifact));
|
|
236
|
+
addCheck(report, "android.dependencies", missingDeps.length === 0, missingDeps.length ? `missing artifacts: ${missingDeps.join(", ")}` : "all expected dependencies found");
|
|
237
|
+
|
|
238
|
+
const actualResValues = extractResValues(moduleGradle);
|
|
239
|
+
const missingResValues: string[] = [];
|
|
240
|
+
for (const [key, expected] of expectedAndroidResValues(resolved)) {
|
|
241
|
+
if (actualResValues.get(key) !== expected) missingResValues.push(`${key}=${expected}`);
|
|
242
|
+
}
|
|
243
|
+
addCheck(report, "android.resValues", missingResValues.length === 0, missingResValues.length ? `missing/mismatch: ${missingResValues.join(", ")}` : "all expected resValue entries found");
|
|
244
|
+
|
|
245
|
+
const firebase = resolved.sdkModules.analytics.firebase;
|
|
246
|
+
if (typeof firebase === "object" && firebase) {
|
|
247
|
+
const moduleRel = applicationModuleRelPath(ctx.projectRoot, ctx.android);
|
|
248
|
+
const fileName = firebase.firebase_file_name || "google-services.json";
|
|
249
|
+
const googleServices = path.join(ctx.projectRoot, moduleRel, fileName);
|
|
250
|
+
addCheck(report, "android.firebaseFile", fs.existsSync(googleServices), path.relative(ctx.projectRoot, googleServices));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function normalizeCode(text: string): string {
|
|
255
|
+
return text.replace(/\s/g, "");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function readDottedPath(obj: Record<string, unknown>, dottedPath: string): unknown {
|
|
259
|
+
let cur: unknown = obj;
|
|
260
|
+
for (const part of dottedPath.split(".")) {
|
|
261
|
+
if (!part) continue;
|
|
262
|
+
if (cur && typeof cur === "object" && part in (cur as Record<string, unknown>)) {
|
|
263
|
+
cur = (cur as Record<string, unknown>)[part];
|
|
264
|
+
} else {
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return cur;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function validateRequiredChannelConfigs(loadedConfigs: LoadedPluginConfig[], channelConfig: Record<string, unknown>): string[] {
|
|
272
|
+
const missing: string[] = [];
|
|
273
|
+
for (const loaded of loadedConfigs) {
|
|
274
|
+
for (const required of loaded.config.requireConfigs ?? []) {
|
|
275
|
+
const value = readDottedPath(channelConfig, required);
|
|
276
|
+
if (value === undefined || value === null || String(value).length === 0) missing.push(`${loaded.config.name}: ${required}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return missing;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function plistUrlSchemes(plist: Record<string, unknown>): string[] {
|
|
283
|
+
const out: string[] = [];
|
|
284
|
+
const types = Array.isArray(plist.CFBundleURLTypes) ? plist.CFBundleURLTypes : [];
|
|
285
|
+
for (const entry of types) {
|
|
286
|
+
if (!entry || typeof entry !== "object") continue;
|
|
287
|
+
const schemes = (entry as Record<string, unknown>).CFBundleURLSchemes;
|
|
288
|
+
if (Array.isArray(schemes)) out.push(...schemes.map(String));
|
|
289
|
+
}
|
|
290
|
+
return out;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function pluginListByName(list: unknown): Map<string, Record<string, unknown>> {
|
|
294
|
+
const out = new Map<string, Record<string, unknown>>();
|
|
295
|
+
if (!Array.isArray(list)) return out;
|
|
296
|
+
for (const item of list) {
|
|
297
|
+
if (!item || typeof item !== "object") continue;
|
|
298
|
+
const rec = item as Record<string, unknown>;
|
|
299
|
+
const name = typeof rec.name === "string" ? rec.name : "";
|
|
300
|
+
if (name) out.set(name, rec);
|
|
301
|
+
}
|
|
302
|
+
return out;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function expectedIosCopiedPaths(loaded: LoadedPluginConfig): string[] {
|
|
306
|
+
const out: string[] = [];
|
|
307
|
+
for (const fw of loaded.config.frameworks ?? []) {
|
|
308
|
+
if (!fw.system) out.push(path.basename(fw.name));
|
|
309
|
+
}
|
|
310
|
+
for (const lib of loaded.config.libs ?? []) {
|
|
311
|
+
if (!lib.system) out.push(path.basename(lib.name));
|
|
312
|
+
}
|
|
313
|
+
for (const source of loaded.config.sources ?? []) {
|
|
314
|
+
if (loaded.config.name === "FirebaseManager" && source === "GoogleService-Info.plist") continue;
|
|
315
|
+
out.push(path.basename(source));
|
|
316
|
+
}
|
|
317
|
+
return [...new Set(out)];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function appDelegateCodeShouldMoveToSceneDelegate(code: CodeConfig): boolean {
|
|
321
|
+
if (code.type === "header") return false;
|
|
322
|
+
const method = code.method ?? "";
|
|
323
|
+
return (
|
|
324
|
+
method.includes("openURL") ||
|
|
325
|
+
method.includes("continueUserActivity") ||
|
|
326
|
+
method.includes("applicationDidEnterBackground") ||
|
|
327
|
+
method.includes("applicationWillEnterForeground")
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function appDelegateCodesForLifecycle(loaded: LoadedPluginConfig, hasUniqueSceneDelegate: boolean): CodeConfig[] {
|
|
332
|
+
const codes = loaded.config.appDelegateCodes ?? [];
|
|
333
|
+
if (!hasUniqueSceneDelegate) return codes;
|
|
334
|
+
return codes.filter((code) => !appDelegateCodeShouldMoveToSceneDelegate(code));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function validateLoadedPluginResourcesForDoctor(loaded: LoadedPluginConfig): string[] {
|
|
338
|
+
const errors = validateLoadedPluginResources(loaded);
|
|
339
|
+
if (loaded.config.name !== "FirebaseManager") return errors;
|
|
340
|
+
return errors.filter((error) => !error.includes("missing source resource for FirebaseManager: GoogleService-Info.plist"));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function checkIos(ctx: WorkspaceContext, report: DoctorReport, config: MeetSdkRemoteConfig): Promise<void> {
|
|
344
|
+
if (!ctx.ios?.ok) {
|
|
345
|
+
addCheck(report, "ios.detect", false, "iOS project not detected");
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const sdkRoot = ctx.iosSdkRoot ?? resolveIosSdkRoot(ctx.packageRoot);
|
|
349
|
+
const channelConfig = buildChannelConfigMap(config);
|
|
350
|
+
const coreConfigs = listSdkCoreConfigs(sdkRoot);
|
|
351
|
+
const pluginConfigs: LoadedPluginConfig[] = [];
|
|
352
|
+
for (const key of unsupportedIosModuleKeys(config)) {
|
|
353
|
+
report.warnings.push(`iOS plugin mapping not found for sdkModules.${key}; skipped`);
|
|
354
|
+
}
|
|
355
|
+
for (const folder of enabledIosPluginFolders(config)) {
|
|
356
|
+
const plugin = findPluginByFolder(sdkRoot, folder);
|
|
357
|
+
if (plugin) pluginConfigs.push(plugin);
|
|
358
|
+
else report.warnings.push(`iOS plugin folder not in SDK package: plugins/${folder}`);
|
|
359
|
+
}
|
|
360
|
+
const loadedConfigs = [...coreConfigs, ...pluginConfigs];
|
|
361
|
+
const resourceErrors = loadedConfigs.flatMap(validateLoadedPluginResourcesForDoctor);
|
|
362
|
+
addCheck(report, "ios.sdkResources", resourceErrors.length === 0, resourceErrors.join(", ") || "SDK resources complete");
|
|
363
|
+
const missingRequired = validateRequiredChannelConfigs(pluginConfigs, channelConfig);
|
|
364
|
+
addCheck(report, "ios.remoteParams", missingRequired.length === 0, missingRequired.join(", ") || "all required plugin params present");
|
|
365
|
+
|
|
366
|
+
const pbxRel = path.relative(ctx.projectRoot, path.join(ctx.ios.xcodeprojPath!, "project.pbxproj")).split(path.sep).join("/");
|
|
367
|
+
const pbxText = readFileIfExists(path.join(ctx.projectRoot, pbxRel));
|
|
368
|
+
const topSdkRoot = path.join(ctx.projectRoot, "topSDK");
|
|
369
|
+
const missingCopied = loadedConfigs.flatMap(expectedIosCopiedPaths).filter((name) => !fs.existsSync(path.join(topSdkRoot, name)));
|
|
370
|
+
addCheck(report, "ios.frameworkFiles", missingCopied.length === 0, missingCopied.length ? `missing in topSDK: ${missingCopied.join(", ")}` : "all expected SDK files copied");
|
|
371
|
+
const missingPbxRefs = loadedConfigs
|
|
372
|
+
.flatMap((loaded) => [...(loaded.config.frameworks ?? []), ...(loaded.config.libs ?? [])].map((x) => x.name))
|
|
373
|
+
.filter((name) => !pbxText.includes(path.basename(name)));
|
|
374
|
+
addCheck(report, "ios.pbxprojReferences", missingPbxRefs.length === 0, missingPbxRefs.length ? `missing refs: ${missingPbxRefs.join(", ")}` : "all expected Xcode references found");
|
|
375
|
+
|
|
376
|
+
const store = new TextFileStore(ctx.projectRoot);
|
|
377
|
+
const pbx = await loadPbxFromStore(store, ctx.projectRoot, ctx.ios.xcodeprojPath!, ctx.ios.targetName ?? path.basename(ctx.ios.xcodeprojPath!, ".xcodeproj"));
|
|
378
|
+
const plistPaths = findInfoPlistPathsFromPbx(pbx);
|
|
379
|
+
addCheck(report, "ios.infoPlist.exists", plistPaths.length > 0, plistPaths.map((p) => path.relative(ctx.projectRoot, p)).join(", ") || "Info.plist not found from pbxproj");
|
|
380
|
+
if (typeof config.sdkModules.analytics.firebase === "object" && config.sdkModules.analytics.firebase) {
|
|
381
|
+
const firebasePlist = plistPaths[0] ? path.join(path.dirname(plistPaths[0]), "GoogleService-Info.plist") : "";
|
|
382
|
+
addCheck(
|
|
383
|
+
report,
|
|
384
|
+
"ios.firebasePlist",
|
|
385
|
+
Boolean(firebasePlist && fs.existsSync(firebasePlist)),
|
|
386
|
+
firebasePlist ? path.relative(ctx.projectRoot, firebasePlist) : "GoogleService-Info.plist target path unavailable"
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
const plist = plistPaths.length ? parsePlistXml(readFileIfExists(plistPaths[0])) : {};
|
|
390
|
+
const topSdk = plist.TOPSDK && typeof plist.TOPSDK === "object" ? plist.TOPSDK as Record<string, unknown> : {};
|
|
391
|
+
addCheck(report, "ios.topSdkAppId", topSdk.APP_ID === config.topsdk.appId, `expected ${config.topsdk.appId}`);
|
|
392
|
+
|
|
393
|
+
const normalPlugins = pluginConfigs.filter((p) => p.config.type !== 4);
|
|
394
|
+
const dataPlugins = pluginConfigs.filter((p) => p.config.type === 4);
|
|
395
|
+
const actualPlugins = pluginListByName(topSdk.Plugins);
|
|
396
|
+
const actualDataPlugins = pluginListByName(topSdk.dataPlugins);
|
|
397
|
+
const missingPluginNames = normalPlugins.map((p) => p.config.name).filter((name) => !actualPlugins.has(name));
|
|
398
|
+
const missingDataPluginNames = dataPlugins.map((p) => p.config.name).filter((name) => !actualDataPlugins.has(name));
|
|
399
|
+
addCheck(report, "ios.topSdkPlugins", missingPluginNames.length + missingDataPluginNames.length === 0, [...missingPluginNames, ...missingDataPluginNames].join(", ") || "all enabled plugins are listed");
|
|
400
|
+
|
|
401
|
+
const missingPluginParams: string[] = [];
|
|
402
|
+
for (const loaded of pluginConfigs) {
|
|
403
|
+
const target = loaded.config.type === 4 ? actualDataPlugins.get(loaded.config.name) : actualPlugins.get(loaded.config.name);
|
|
404
|
+
const params = target?.params && typeof target.params === "object" ? target.params as Record<string, unknown> : {};
|
|
405
|
+
for (const [key, raw] of Object.entries(loaded.config.pluginParams ?? {})) {
|
|
406
|
+
const expected = applyChannelTemplate(String(raw), channelConfig);
|
|
407
|
+
if (params[key] !== expected) missingPluginParams.push(`${loaded.config.name}.${key}=${expected}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
addCheck(report, "ios.pluginParams", missingPluginParams.length === 0, missingPluginParams.join(", ") || "all plugin params match");
|
|
411
|
+
|
|
412
|
+
const schemes = plistUrlSchemes(plist);
|
|
413
|
+
const expectedSchemes: string[] = [];
|
|
414
|
+
for (const loaded of loadedConfigs) {
|
|
415
|
+
if (!loaded.config.urlScheme) continue;
|
|
416
|
+
let scheme = applyChannelTemplate(loaded.config.urlScheme, channelConfig);
|
|
417
|
+
if (scheme.endsWith("://")) scheme = scheme.replace("://", "");
|
|
418
|
+
if (scheme) expectedSchemes.push(scheme);
|
|
419
|
+
}
|
|
420
|
+
const missingSchemes = expectedSchemes.filter((scheme) => !schemes.includes(scheme));
|
|
421
|
+
addCheck(report, "ios.urlSchemes.present", missingSchemes.length === 0, missingSchemes.join(", ") || "all expected URL schemes present");
|
|
422
|
+
|
|
423
|
+
const missingInfoParams: string[] = [];
|
|
424
|
+
for (const loaded of loadedConfigs) {
|
|
425
|
+
for (const [key, raw] of Object.entries(loaded.config.infoParams ?? {})) {
|
|
426
|
+
const expected = applyChannelTemplateValue(raw, channelConfig);
|
|
427
|
+
if (JSON.stringify(plist[key]) !== JSON.stringify(expected)) missingInfoParams.push(`${loaded.config.name}.${key}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
addCheck(report, "ios.infoParams", missingInfoParams.length === 0, missingInfoParams.join(", ") || "all Info.plist params match");
|
|
431
|
+
|
|
432
|
+
const scenes = findSceneDelegateFiles(pbx.srcRoot);
|
|
433
|
+
const hasUniqueSceneDelegate = scenes.length === 1;
|
|
434
|
+
const delegates = findDelegateFiles(pbx.srcRoot);
|
|
435
|
+
const appDelegateMissing: string[] = [];
|
|
436
|
+
if (delegates.length === 1) {
|
|
437
|
+
const text = normalizeCode(readFileIfExists(delegates[0]));
|
|
438
|
+
for (const loaded of loadedConfigs) {
|
|
439
|
+
for (const code of appDelegateCodesForLifecycle(loaded, hasUniqueSceneDelegate)) {
|
|
440
|
+
if (!text.includes(normalizeCode(code.content))) appDelegateMissing.push(`${loaded.config.name}: ${code.content}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
} else {
|
|
444
|
+
appDelegateMissing.push(delegates.length ? `multiple delegates: ${delegates.join(", ")}` : "no UIApplicationDelegate .m/.mm found");
|
|
445
|
+
}
|
|
446
|
+
addCheck(report, "ios.appDelegateInjection", appDelegateMissing.length === 0, appDelegateMissing.join(", ") || "AppDelegate injection complete");
|
|
447
|
+
|
|
448
|
+
const sceneMissing: string[] = [];
|
|
449
|
+
if (scenes.length === 1) {
|
|
450
|
+
const text = normalizeCode(readFileIfExists(scenes[0]));
|
|
451
|
+
for (const loaded of loadedConfigs) {
|
|
452
|
+
for (const code of loaded.config.sceneDelegateCodes ?? []) {
|
|
453
|
+
if (!text.includes(normalizeCode(code.content))) sceneMissing.push(`${loaded.config.name}: ${code.content}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
} else if (scenes.length > 1) {
|
|
457
|
+
sceneMissing.push(`multiple scene delegates: ${scenes.join(", ")}`);
|
|
458
|
+
}
|
|
459
|
+
addCheck(report, "ios.sceneDelegateInjection", sceneMissing.length === 0, sceneMissing.join(", ") || "SceneDelegate injection complete or not required");
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export async function runDoctor(ctx: WorkspaceContext, platform: "android" | "ios"): Promise<DoctorReport> {
|
|
463
|
+
const configPath = ctx.remoteConfigPath ?? path.join(ctx.projectRoot, MEETSDK_REMOTE_CONFIG_FILENAME);
|
|
464
|
+
const report: DoctorReport = {
|
|
465
|
+
projectRoot: ctx.projectRoot,
|
|
466
|
+
platform,
|
|
467
|
+
configPath,
|
|
468
|
+
ok: false,
|
|
469
|
+
checks: [],
|
|
470
|
+
warnings: [],
|
|
471
|
+
errors: [],
|
|
472
|
+
};
|
|
473
|
+
const config = readJsonConfig(configPath);
|
|
474
|
+
addCheck(report, "remoteConfig.existsAndParses", Boolean(config), configPath);
|
|
475
|
+
if (!config) {
|
|
476
|
+
report.ok = false;
|
|
477
|
+
return report;
|
|
478
|
+
}
|
|
479
|
+
const validation = validateMeetSdkRemoteConfig(config);
|
|
480
|
+
addCheck(report, "remoteConfig.requiredFields", validation.ok, validation.ok ? "required fields present" : validation.missing.join(", "));
|
|
481
|
+
if (platform === "android") await checkAndroid(ctx, report, config);
|
|
482
|
+
else await checkIos(ctx, report, config);
|
|
483
|
+
report.ok = report.errors.length === 0;
|
|
484
|
+
return report;
|
|
485
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
export function generatePatchForFile(relativePath: string, originalContent: string, updatedContent: string): string {
|
|
7
|
+
if (originalContent === updatedContent) return "";
|
|
8
|
+
|
|
9
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "meetgames-"));
|
|
10
|
+
const leftPath = path.join(tempRoot, "left", relativePath);
|
|
11
|
+
const rightPath = path.join(tempRoot, "right", relativePath);
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
fs.mkdirSync(path.dirname(leftPath), { recursive: true });
|
|
15
|
+
fs.mkdirSync(path.dirname(rightPath), { recursive: true });
|
|
16
|
+
fs.writeFileSync(leftPath, originalContent);
|
|
17
|
+
fs.writeFileSync(rightPath, updatedContent);
|
|
18
|
+
|
|
19
|
+
let diffOutput = "";
|
|
20
|
+
try {
|
|
21
|
+
diffOutput = execFileSync("git", ["diff", "--no-index", "--text", "--", leftPath, rightPath], {
|
|
22
|
+
encoding: "utf8",
|
|
23
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
24
|
+
});
|
|
25
|
+
} catch (error: unknown) {
|
|
26
|
+
const err = error as { status?: number; stdout?: string };
|
|
27
|
+
if (err && err.status === 1 && typeof err.stdout === "string") {
|
|
28
|
+
diffOutput = err.stdout;
|
|
29
|
+
} else {
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return normalizePatchHeaders(diffOutput, relativePath);
|
|
35
|
+
} finally {
|
|
36
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizePatchHeaders(patchText: string, relativePath: string): string {
|
|
41
|
+
const lines = patchText.split("\n");
|
|
42
|
+
const out: string[] = [];
|
|
43
|
+
let headerReplaced = 0;
|
|
44
|
+
|
|
45
|
+
for (const line of lines) {
|
|
46
|
+
if (line.startsWith("diff --git ")) {
|
|
47
|
+
out.push(`diff --git a/${relativePath} b/${relativePath}`);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (line.startsWith("--- ") && headerReplaced === 0) {
|
|
51
|
+
out.push(`--- a/${relativePath}`);
|
|
52
|
+
headerReplaced += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (line.startsWith("+++ ") && headerReplaced === 1) {
|
|
56
|
+
out.push(`+++ b/${relativePath}`);
|
|
57
|
+
headerReplaced += 1;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
out.push(line);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return out.join("\n");
|
|
64
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { Manifest, ManifestStep, PipelineReport, StepResult, WorkspaceContext } from "../contracts/types.js";
|
|
4
|
+
import { getOpHandler, type BinaryCopy } from "../ops/handlers.js";
|
|
5
|
+
import { TextFileStore } from "../ops/fileStore.js";
|
|
6
|
+
import { generatePatchForFile } from "./patch.js";
|
|
7
|
+
|
|
8
|
+
function stepPlatform(step: ManifestStep): "android" | "ios" | "all" {
|
|
9
|
+
return step.platform ?? "all";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function shouldRunStep(ctx: WorkspaceContext, step: ManifestStep): { run: boolean; reason?: string } {
|
|
13
|
+
const pf = stepPlatform(step);
|
|
14
|
+
if (pf === "all") return { run: true };
|
|
15
|
+
if (pf === "android") {
|
|
16
|
+
if (!ctx.android?.ok) return { run: false, reason: "android not detected" };
|
|
17
|
+
return { run: true };
|
|
18
|
+
}
|
|
19
|
+
if (pf === "ios") {
|
|
20
|
+
if (!ctx.ios?.ok) return { run: false, reason: "ios not detected" };
|
|
21
|
+
return { run: true };
|
|
22
|
+
}
|
|
23
|
+
return { run: true };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface RunPipelineOptions {
|
|
27
|
+
dryRun: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RunPipelineResult {
|
|
31
|
+
report: PipelineReport;
|
|
32
|
+
patch: string;
|
|
33
|
+
binaryCopies: BinaryCopy[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function applyBinaryCopies(projectRoot: string, copies: BinaryCopy[]): void {
|
|
37
|
+
for (const c of copies) {
|
|
38
|
+
const dest = path.join(projectRoot, c.relTo);
|
|
39
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
40
|
+
if (fs.statSync(c.fromAbs).isDirectory()) {
|
|
41
|
+
fs.cpSync(c.fromAbs, dest, { recursive: true });
|
|
42
|
+
} else {
|
|
43
|
+
fs.copyFileSync(c.fromAbs, dest);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function runPipeline(
|
|
49
|
+
ctx: WorkspaceContext,
|
|
50
|
+
manifest: Manifest,
|
|
51
|
+
options: RunPipelineOptions
|
|
52
|
+
): Promise<RunPipelineResult> {
|
|
53
|
+
const results: StepResult[] = [];
|
|
54
|
+
const warnings: string[] = [];
|
|
55
|
+
const errors: string[] = [];
|
|
56
|
+
let stepsRun = 0;
|
|
57
|
+
let stepsSkipped = 0;
|
|
58
|
+
|
|
59
|
+
const store = new TextFileStore(ctx.projectRoot);
|
|
60
|
+
const binaryCopies: BinaryCopy[] = [];
|
|
61
|
+
|
|
62
|
+
for (const step of manifest.steps) {
|
|
63
|
+
const decision = shouldRunStep(ctx, step);
|
|
64
|
+
if (!decision.run) {
|
|
65
|
+
stepsSkipped += 1;
|
|
66
|
+
warnings.push(`skipped op=${step.op}: ${decision.reason ?? "platform mismatch"}`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const handler = getOpHandler(step.op);
|
|
70
|
+
if (!handler) {
|
|
71
|
+
const r: StepResult = { ok: false, changedFiles: [], warnings: [], errors: [`unknown op: ${step.op}`] };
|
|
72
|
+
results.push(r);
|
|
73
|
+
errors.push(...r.errors);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
const r = await Promise.resolve(handler(ctx, store, step.args ?? {}, options.dryRun, binaryCopies));
|
|
77
|
+
results.push(r);
|
|
78
|
+
stepsRun += 1;
|
|
79
|
+
warnings.push(...r.warnings);
|
|
80
|
+
if (!r.ok) {
|
|
81
|
+
errors.push(...r.errors);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const patchParts: string[] = [];
|
|
87
|
+
for (const { rel, original, current } of store.changedEntries()) {
|
|
88
|
+
patchParts.push(generatePatchForFile(rel, original, current));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!options.dryRun && errors.length === 0) {
|
|
92
|
+
store.flushToDisk();
|
|
93
|
+
applyBinaryCopies(ctx.projectRoot, binaryCopies);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const report: PipelineReport = {
|
|
97
|
+
dryRun: options.dryRun,
|
|
98
|
+
projectRoot: ctx.projectRoot,
|
|
99
|
+
stepsRun,
|
|
100
|
+
stepsSkipped,
|
|
101
|
+
results,
|
|
102
|
+
warnings,
|
|
103
|
+
errors,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return { report, patch: patchParts.filter(Boolean).join("\n"), binaryCopies };
|
|
107
|
+
}
|