@leonxin/meetgames 0.1.7 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/.agents/skills/meet-sdk-regression/SKILL.md +93 -0
  2. package/.cursor/mcp.example.json +16 -0
  3. package/.cursor/mcp.json +11 -0
  4. package/.cursor/skills/meetgames-mcp/SKILL.md +48 -0
  5. package/.vite/vitest/results.json +1 -0
  6. package/README.md +31 -8
  7. package/dist/aab-converter/aab-entry.d.ts +3 -0
  8. package/dist/aab-converter/aab-entry.d.ts.map +1 -0
  9. package/dist/aab-converter/aab-entry.js +49 -0
  10. package/dist/aab-converter/aab-entry.js.map +1 -0
  11. package/dist/aab-converter/apksExtractor.d.ts +2 -0
  12. package/dist/aab-converter/apksExtractor.d.ts.map +1 -0
  13. package/dist/aab-converter/apksExtractor.js +108 -0
  14. package/dist/aab-converter/apksExtractor.js.map +1 -0
  15. package/dist/aab-converter/bundletoolRunner.d.ts +15 -0
  16. package/dist/aab-converter/bundletoolRunner.d.ts.map +1 -0
  17. package/dist/aab-converter/bundletoolRunner.js +46 -0
  18. package/dist/aab-converter/bundletoolRunner.js.map +1 -0
  19. package/dist/aab-converter/cliArgs.d.ts +27 -0
  20. package/dist/aab-converter/cliArgs.d.ts.map +1 -0
  21. package/dist/aab-converter/cliArgs.js +170 -0
  22. package/dist/aab-converter/cliArgs.js.map +1 -0
  23. package/dist/aab-converter/convertAabToApk.d.ts +7 -0
  24. package/dist/aab-converter/convertAabToApk.d.ts.map +1 -0
  25. package/dist/aab-converter/convertAabToApk.js +69 -0
  26. package/dist/aab-converter/convertAabToApk.js.map +1 -0
  27. package/dist/aab-converter/resourcePaths.d.ts +4 -0
  28. package/dist/aab-converter/resourcePaths.d.ts.map +1 -0
  29. package/dist/aab-converter/resourcePaths.js +42 -0
  30. package/dist/aab-converter/resourcePaths.js.map +1 -0
  31. package/dist/aab-converter/signingOptions.d.ts +9 -0
  32. package/dist/aab-converter/signingOptions.d.ts.map +1 -0
  33. package/dist/aab-converter/signingOptions.js +21 -0
  34. package/dist/aab-converter/signingOptions.js.map +1 -0
  35. package/dist/aab-converter/types.d.ts +24 -0
  36. package/dist/aab-converter/types.d.ts.map +1 -0
  37. package/dist/aab-converter/types.js +2 -0
  38. package/dist/aab-converter/types.js.map +1 -0
  39. package/dist/android/adapter.d.ts.map +1 -1
  40. package/dist/android/adapter.js +2 -2
  41. package/dist/android/adapter.js.map +1 -1
  42. package/dist/android/detect.d.ts +2 -2
  43. package/dist/android/detect.d.ts.map +1 -1
  44. package/dist/android/detect.js +36 -8
  45. package/dist/android/detect.js.map +1 -1
  46. package/dist/android/meetSdkRemoteGradle.d.ts +0 -3
  47. package/dist/android/meetSdkRemoteGradle.d.ts.map +1 -1
  48. package/dist/android/meetSdkRemoteGradle.js +13 -20
  49. package/dist/android/meetSdkRemoteGradle.js.map +1 -1
  50. package/dist/cli.d.ts.map +1 -1
  51. package/dist/cli.js +157 -31
  52. package/dist/cli.js.map +1 -1
  53. package/dist/config/meetSdkDefaultConfig.d.ts +19 -2
  54. package/dist/config/meetSdkDefaultConfig.d.ts.map +1 -1
  55. package/dist/config/meetSdkDefaultConfig.js +67 -5
  56. package/dist/config/meetSdkDefaultConfig.js.map +1 -1
  57. package/dist/config/meetSdkIosConfig.d.ts +21 -0
  58. package/dist/config/meetSdkIosConfig.d.ts.map +1 -0
  59. package/dist/config/meetSdkIosConfig.js +66 -0
  60. package/dist/config/meetSdkIosConfig.js.map +1 -0
  61. package/dist/config/meetSdkRemoteConfig.d.ts +19 -11
  62. package/dist/config/meetSdkRemoteConfig.d.ts.map +1 -1
  63. package/dist/config/meetSdkRemoteConfig.js +89 -69
  64. package/dist/config/meetSdkRemoteConfig.js.map +1 -1
  65. package/dist/config/topsdkFeatureModules.d.ts +5 -0
  66. package/dist/config/topsdkFeatureModules.d.ts.map +1 -1
  67. package/dist/config/topsdkFeatureModules.js +26 -0
  68. package/dist/config/topsdkFeatureModules.js.map +1 -1
  69. package/dist/contracts/types.d.ts +19 -6
  70. package/dist/contracts/types.d.ts.map +1 -1
  71. package/dist/core/doctor.d.ts +17 -0
  72. package/dist/core/doctor.d.ts.map +1 -0
  73. package/dist/core/doctor.js +444 -0
  74. package/dist/core/doctor.js.map +1 -0
  75. package/dist/core/pipeline.d.ts.map +1 -1
  76. package/dist/core/pipeline.js +0 -15
  77. package/dist/core/pipeline.js.map +1 -1
  78. package/dist/core/platform.d.ts +12 -0
  79. package/dist/core/platform.d.ts.map +1 -0
  80. package/dist/core/platform.js +40 -0
  81. package/dist/core/platform.js.map +1 -0
  82. package/dist/core/reporter.js +1 -1
  83. package/dist/core/reporter.js.map +1 -1
  84. package/dist/core/workspace.d.ts +2 -2
  85. package/dist/core/workspace.d.ts.map +1 -1
  86. package/dist/core/workspace.js +4 -5
  87. package/dist/core/workspace.js.map +1 -1
  88. package/dist/index.d.ts +3 -1
  89. package/dist/index.d.ts.map +1 -1
  90. package/dist/index.js +3 -1
  91. package/dist/index.js.map +1 -1
  92. package/dist/ios/channelConfig.d.ts +1 -0
  93. package/dist/ios/channelConfig.d.ts.map +1 -1
  94. package/dist/ios/channelConfig.js +82 -0
  95. package/dist/ios/channelConfig.js.map +1 -1
  96. package/dist/ios/codeUtils.d.ts +1 -0
  97. package/dist/ios/codeUtils.d.ts.map +1 -1
  98. package/dist/ios/codeUtils.js +11 -2
  99. package/dist/ios/codeUtils.js.map +1 -1
  100. package/dist/ios/detect.d.ts +2 -2
  101. package/dist/ios/detect.d.ts.map +1 -1
  102. package/dist/ios/detect.js +49 -10
  103. package/dist/ios/detect.js.map +1 -1
  104. package/dist/ios/entitlements.d.ts +4 -0
  105. package/dist/ios/entitlements.d.ts.map +1 -0
  106. package/dist/ios/entitlements.js +53 -0
  107. package/dist/ios/entitlements.js.map +1 -0
  108. package/dist/ios/fileManager.d.ts.map +1 -1
  109. package/dist/ios/fileManager.js +3 -2
  110. package/dist/ios/fileManager.js.map +1 -1
  111. package/dist/ios/infoPlist.d.ts +1 -1
  112. package/dist/ios/infoPlist.d.ts.map +1 -1
  113. package/dist/ios/infoPlist.js.map +1 -1
  114. package/dist/ios/integrate.d.ts.map +1 -1
  115. package/dist/ios/integrate.js +211 -36
  116. package/dist/ios/integrate.js.map +1 -1
  117. package/dist/ios/pbxprojEditor.d.ts +2 -0
  118. package/dist/ios/pbxprojEditor.d.ts.map +1 -1
  119. package/dist/ios/pbxprojEditor.js +179 -1
  120. package/dist/ios/pbxprojEditor.js.map +1 -1
  121. package/dist/ios/pluginConfig.d.ts +1 -0
  122. package/dist/ios/pluginConfig.d.ts.map +1 -1
  123. package/dist/ios/pluginConfig.js +36 -4
  124. package/dist/ios/pluginConfig.js.map +1 -1
  125. package/dist/ios/sdkBundle.d.ts +1 -1
  126. package/dist/ios/sdkBundle.d.ts.map +1 -1
  127. package/dist/ios/sdkBundle.js +7 -5
  128. package/dist/ios/sdkBundle.js.map +1 -1
  129. package/dist/ios/template.d.ts +1 -0
  130. package/dist/ios/template.d.ts.map +1 -1
  131. package/dist/ios/template.js +14 -1
  132. package/dist/ios/template.js.map +1 -1
  133. package/dist/ios/types.d.ts +2 -2
  134. package/dist/ios/types.d.ts.map +1 -1
  135. package/dist/mcp/server.d.ts.map +1 -1
  136. package/dist/mcp/server.js +14 -13
  137. package/dist/mcp/server.js.map +1 -1
  138. package/dist/mcp/service.d.ts +8 -6
  139. package/dist/mcp/service.d.ts.map +1 -1
  140. package/dist/mcp/service.js +34 -14
  141. package/dist/mcp/service.js.map +1 -1
  142. package/dist/ops/handlers.d.ts.map +1 -1
  143. package/dist/ops/handlers.js +10 -4
  144. package/dist/ops/handlers.js.map +1 -1
  145. package/dist/remote/sdkHomeDownload.d.ts +65 -0
  146. package/dist/remote/sdkHomeDownload.d.ts.map +1 -0
  147. package/dist/remote/sdkHomeDownload.js +208 -0
  148. package/dist/remote/sdkHomeDownload.js.map +1 -0
  149. package/dist/remote/topsdkDownloadSdkConfig.d.ts.map +1 -1
  150. package/dist/remote/topsdkDownloadSdkConfig.js +11 -1
  151. package/dist/remote/topsdkDownloadSdkConfig.js.map +1 -1
  152. package/dist/shared/errors.d.ts +7 -0
  153. package/dist/shared/errors.d.ts.map +1 -0
  154. package/dist/shared/errors.js +16 -0
  155. package/dist/shared/errors.js.map +1 -0
  156. package/dist/shared/fileUtils.d.ts +5 -0
  157. package/dist/shared/fileUtils.d.ts.map +1 -0
  158. package/dist/shared/fileUtils.js +35 -0
  159. package/dist/shared/fileUtils.js.map +1 -0
  160. package/dist/shared/logger.d.ts +10 -0
  161. package/dist/shared/logger.d.ts.map +1 -0
  162. package/dist/shared/logger.js +37 -0
  163. package/dist/shared/logger.js.map +1 -0
  164. package/dist/shared/pathUtils.d.ts +4 -0
  165. package/dist/shared/pathUtils.d.ts.map +1 -0
  166. package/dist/shared/pathUtils.js +22 -0
  167. package/dist/shared/pathUtils.js.map +1 -0
  168. package/dist/shared/processRunner.d.ts +12 -0
  169. package/dist/shared/processRunner.d.ts.map +1 -0
  170. package/dist/shared/processRunner.js +31 -0
  171. package/dist/shared/processRunner.js.map +1 -0
  172. package/docs/AAB_CONVERTER_CLI_PLAN.md +392 -0
  173. package/docs/API.md +246 -32
  174. package/docs/CLI.md +292 -0
  175. package/docs/INTEGRATION.md +116 -0
  176. package/docs/MCP.md +86 -0
  177. package/docs/README.md +19 -10
  178. package/docs/{api → archive/api}/downloadSDKConfig.md +8 -6
  179. package/docs/{api → archive/api}/getChannelConfig-meetgames.md +1 -1
  180. package/docs/archive/ios-migration.md +2139 -0
  181. 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
  182. package/docs/{ → archive/product/}/351/234/200/346/261/202/346/226/207/346/241/243.md +15 -14
  183. package/logs/convert-20260622-155037.log +5 -0
  184. package/logs/convert-20260622-155226.log +6 -0
  185. package/meetsdk-android.json +2 -1
  186. package/meetsdk-ios.json +15 -0
  187. package/package.json +10 -35
  188. package/scripts/package-aab-cli-win.mjs +193 -0
  189. package/src/aab-converter/aab-entry.ts +48 -0
  190. package/src/aab-converter/apksExtractor.ts +119 -0
  191. package/src/aab-converter/bundletoolRunner.ts +63 -0
  192. package/src/aab-converter/cliArgs.ts +194 -0
  193. package/src/aab-converter/convertAabToApk.ts +81 -0
  194. package/src/aab-converter/resourcePaths.ts +43 -0
  195. package/src/aab-converter/signingOptions.ts +29 -0
  196. package/src/aab-converter/types.ts +26 -0
  197. package/src/android/adapter.ts +9 -0
  198. package/src/android/assembleIntegrationJson.ts +33 -0
  199. package/src/android/detect.ts +132 -0
  200. package/src/android/downloadGoogleServicesJson.ts +56 -0
  201. package/src/android/gradle.ts +116 -0
  202. package/src/android/manifest.ts +50 -0
  203. package/src/android/meetSdkRemoteGradle.ts +837 -0
  204. package/src/cli.ts +488 -0
  205. package/src/config/fetchConfigWrite.ts +30 -0
  206. package/src/config/loadAndroidIntegration.ts +41 -0
  207. package/src/config/loadManifest.ts +40 -0
  208. package/src/config/meetSdkDefaultConfig.ts +99 -0
  209. package/src/config/meetSdkIosConfig.ts +87 -0
  210. package/src/config/meetSdkRemoteConfig.ts +1211 -0
  211. package/src/config/topsdkFeatureModules.ts +92 -0
  212. package/src/contracts/types.ts +121 -0
  213. package/src/core/doctor.ts +485 -0
  214. package/src/core/patch.ts +64 -0
  215. package/src/core/pipeline.ts +107 -0
  216. package/src/core/platform.ts +47 -0
  217. package/src/core/previewPatches.ts +23 -0
  218. package/src/core/reporter.ts +24 -0
  219. package/src/core/workspace.ts +29 -0
  220. package/src/entry.ts +7 -0
  221. package/src/index.ts +133 -0
  222. package/src/ios/channelConfig.ts +128 -0
  223. package/src/ios/codeUtils.ts +160 -0
  224. package/src/ios/detect.ts +105 -0
  225. package/src/ios/entitlements.ts +61 -0
  226. package/src/ios/fileManager.ts +48 -0
  227. package/src/ios/infoPlist.ts +55 -0
  228. package/src/ios/integrate.ts +516 -0
  229. package/src/ios/pbxprojEditor.ts +383 -0
  230. package/src/ios/pluginConfig.ts +97 -0
  231. package/src/ios/reserved.ts +8 -0
  232. package/src/ios/sdkBundle.ts +36 -0
  233. package/src/ios/template.ts +36 -0
  234. package/src/ios/types.ts +65 -0
  235. package/src/mcp/server.ts +170 -0
  236. package/src/mcp/service.ts +222 -0
  237. package/src/mcp-entry.ts +7 -0
  238. package/src/ops/fileStore.ts +56 -0
  239. package/src/ops/handlers.ts +304 -0
  240. package/src/remote/fetchJson.ts +22 -0
  241. package/src/remote/sdkHomeDownload.ts +274 -0
  242. package/src/remote/topsdkDownloadSdkConfig.ts +93 -0
  243. package/src/remote/topsdkGetSdkConfig.ts +122 -0
  244. package/src/remote/topsdkSign.ts +10 -0
  245. package/src/shared/errors.ts +16 -0
  246. package/src/shared/fileUtils.ts +41 -0
  247. package/src/shared/logger.ts +49 -0
  248. package/src/shared/pathUtils.ts +24 -0
  249. package/src/shared/processRunner.ts +43 -0
  250. package/test-projects/README.md +51 -0
  251. package/test-projects/_preview/pipeline.patch +281 -0
  252. package/tests/aab-converter.test.ts +213 -0
  253. package/tests/assemble.test.ts +12 -0
  254. package/tests/doctor.test.ts +89 -0
  255. package/tests/downloadGoogleServicesJson.test.ts +47 -0
  256. package/tests/fetch-remote.test.ts +23 -0
  257. package/tests/fetchConfigOverrides.test.ts +28 -0
  258. package/tests/fetchConfigWrite.test.ts +54 -0
  259. package/tests/gradle.test.ts +33 -0
  260. package/tests/integration-json.test.ts +29 -0
  261. package/tests/ios.codeUtils.test.ts +23 -0
  262. package/tests/ios.sdkBundle.test.ts +16 -0
  263. package/tests/loadManifest.test.ts +15 -0
  264. package/tests/manifest-xml.test.ts +30 -0
  265. package/tests/mcp.e2e.ts +217 -0
  266. package/tests/mcp.service.test.ts +53 -0
  267. package/tests/meetSdkRemoteConfig.test.ts +456 -0
  268. package/tests/meetSdkRemoteGradle.test.ts +414 -0
  269. package/tests/pipeline.android.test.ts +96 -0
  270. package/tests/pipeline.integration-json.test.ts +58 -0
  271. package/tests/pipeline.ios.test.ts +385 -0
  272. package/tests/pipeline.preview.patch.test.ts +85 -0
  273. package/tests/platformSelection.test.ts +77 -0
  274. package/tests/sdkHomeDownload.test.ts +124 -0
  275. package/tests/sdkVersionConfig.test.ts +130 -0
  276. package/tests/test-projects-hosts.test.ts +78 -0
  277. package/tests/topsdk.test.ts +53 -0
  278. package/tests/topsdkDownloadSdkConfig.test.ts +81 -0
  279. package/tests/topsdkFeatureModules.test.ts +116 -0
  280. package/tsconfig.json +19 -0
  281. package/vitest.config.ts +9 -0
  282. package/vitest.mcp.config.ts +11 -0
  283. package/bundled/android/sample.txt +0 -1
  284. package/docs/ANDROID.md +0 -133
  285. package/docs/CURSOR-MCP-SETUP.md +0 -72
  286. package/docs/MCP-SKILL.md +0 -63
  287. package/fixtures/api-samples/getChannelConfig-meetgames.sample.json +0 -123
  288. package/fixtures/meetsdk-remote-config.download-shape.json +0 -20
  289. package/fixtures/meetsdk-remote-config.mock.json +0 -69
  290. package/fixtures/recipes/android-default.fixture.yaml +0 -15
  291. package/fixtures/recipes/android-integration.fixture.json +0 -29
  292. package/fixtures/topsdk-config-reference.json +0 -39
  293. /package/docs/{api → archive/api}/getSDKConfig.md +0 -0
@@ -0,0 +1,61 @@
1
+ import path from "node:path";
2
+ import type { TextFileStore } from "../ops/fileStore.js";
3
+ import { buildPlistXml, parsePlistXml } from "./infoPlist.js";
4
+ import { getTargetBuildSettings, setBuildSetting, type PbxContext } from "./pbxprojEditor.js";
5
+
6
+ const APPLE_SIGN_IN_KEY = "com.apple.developer.applesignin";
7
+
8
+ function unquote(value: string): string {
9
+ return value.replace(/^"|"$/g, "").trim();
10
+ }
11
+
12
+ function entitlementsRelPath(pbx: PbxContext, raw: unknown): string | null {
13
+ if (typeof raw !== "string" || !raw.trim()) return null;
14
+ let rel = unquote(raw);
15
+ if (rel.includes("$(SRCROOT)")) {
16
+ rel = rel.replace(/\$\(SRCROOT\)/g, pbx.srcRoot);
17
+ }
18
+ if (path.isAbsolute(rel)) {
19
+ return path.relative(pbx.srcRoot, rel).split(path.sep).join("/");
20
+ }
21
+ return rel.split(path.sep).join("/");
22
+ }
23
+
24
+ function readPlistOrEmpty(store: TextFileStore, relFromProjectRoot: string): Record<string, unknown> {
25
+ const xml = store.read(relFromProjectRoot);
26
+ if (!xml.trim()) return {};
27
+ return parsePlistXml(xml);
28
+ }
29
+
30
+ export function ensureAppleSignInEntitlement(
31
+ store: TextFileStore,
32
+ projectRoot: string,
33
+ pbx: PbxContext
34
+ ): string[] {
35
+ const settings = getTargetBuildSettings(pbx);
36
+ const entRelPaths = new Set<string>();
37
+ for (const s of settings) {
38
+ const rel = entitlementsRelPath(pbx, s.CODE_SIGN_ENTITLEMENTS);
39
+ if (rel) entRelPaths.add(rel);
40
+ }
41
+
42
+ if (!entRelPaths.size) {
43
+ const rel = path.join(pbx.targetName, `${pbx.targetName}.entitlements`).split(path.sep).join("/");
44
+ setBuildSetting(pbx, "CODE_SIGN_ENTITLEMENTS", rel);
45
+ entRelPaths.add(rel);
46
+ }
47
+
48
+ const changedRelPaths: string[] = [];
49
+ for (const entRel of entRelPaths) {
50
+ const abs = path.isAbsolute(entRel) ? entRel : path.join(pbx.srcRoot, entRel);
51
+ const storeRel = path.relative(projectRoot, abs).split(path.sep).join("/");
52
+ const data = readPlistOrEmpty(store, storeRel);
53
+ const before = JSON.stringify(data);
54
+ data[APPLE_SIGN_IN_KEY] = ["Default"];
55
+ if (JSON.stringify(data) !== before || !store.read(storeRel).trim()) {
56
+ store.write(storeRel, buildPlistXml(data));
57
+ changedRelPaths.push(storeRel);
58
+ }
59
+ }
60
+ return changedRelPaths;
61
+ }
@@ -0,0 +1,48 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /** Copies SDK assets into `{srcRoot}/topSDK/` (topsdk-tool-ios FileManager). */
5
+ export class TopSdkFileManager {
6
+ readonly topSdkRel = "topSDK";
7
+ readonly topSdkAbs: string;
8
+
9
+ constructor(readonly srcRoot: string) {
10
+ this.topSdkAbs = path.join(srcRoot, this.topSdkRel);
11
+ fs.mkdirSync(this.topSdkAbs, { recursive: true });
12
+ }
13
+
14
+ relFromSrc(absPath: string): string {
15
+ return path.relative(this.srcRoot, absPath).split(path.sep).join("/");
16
+ }
17
+
18
+ copyFramework(srcPath: string): string {
19
+ const base = path.basename(srcPath);
20
+ if (!base.includes(".framework") && !base.includes(".xcframework") && !base.includes(".bundle")) {
21
+ throw new Error(`not a framework/xcframework/bundle: ${srcPath}`);
22
+ }
23
+ if (!fs.existsSync(srcPath)) throw new Error(`framework not found: ${srcPath}`);
24
+ const dest = path.join(this.topSdkAbs, base);
25
+ fs.rmSync(dest, { recursive: true, force: true });
26
+ fs.cpSync(srcPath, dest, { recursive: true });
27
+ return dest;
28
+ }
29
+
30
+ copyDir(srcPath: string): string {
31
+ if (!fs.existsSync(srcPath)) throw new Error(`directory not found: ${srcPath}`);
32
+ const base = path.basename(srcPath);
33
+ const dest = path.join(this.topSdkAbs, base);
34
+ fs.rmSync(dest, { recursive: true, force: true });
35
+ fs.cpSync(srcPath, dest, { recursive: true });
36
+ return dest;
37
+ }
38
+
39
+ copyFile(srcPath: string): string {
40
+ if (!fs.existsSync(srcPath) || !fs.statSync(srcPath).isFile()) {
41
+ throw new Error(`file not found: ${srcPath}`);
42
+ }
43
+ const dest = path.join(this.topSdkAbs, path.basename(srcPath));
44
+ fs.rmSync(dest, { force: true });
45
+ fs.copyFileSync(srcPath, dest);
46
+ return dest;
47
+ }
48
+ }
@@ -0,0 +1,55 @@
1
+ import { build as buildPlist, parse as parsePlist } from "plist";
2
+
3
+ export interface PlistDocument {
4
+ relPath: string;
5
+ data: Record<string, unknown>;
6
+ }
7
+
8
+ export function parsePlistXml(xml: string): Record<string, unknown> {
9
+ const parsed = parsePlist(xml);
10
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
11
+ return {};
12
+ }
13
+ return parsed as Record<string, unknown>;
14
+ }
15
+
16
+ export function buildPlistXml(data: Record<string, unknown>): string {
17
+ return buildPlist(data as Parameters<typeof buildPlist>[0]);
18
+ }
19
+
20
+ export function addPlistParam(data: Record<string, unknown>, key: string, value: unknown): void {
21
+ data[key] = value;
22
+ }
23
+
24
+ export function setAppTransportSecurity(data: Record<string, unknown>, open: boolean): void {
25
+ const security =
26
+ (data.NSAppTransportSecurity as Record<string, unknown> | undefined) ?? {};
27
+ security.NSAllowsArbitraryLoads = open;
28
+ data.NSAppTransportSecurity = security;
29
+ }
30
+
31
+ export function addQueriesScheme(data: Record<string, unknown>, scheme: string): void {
32
+ if (!scheme) return;
33
+ const schemes = Array.isArray(data.LSApplicationQueriesSchemes)
34
+ ? [...(data.LSApplicationQueriesSchemes as string[])]
35
+ : [];
36
+ if (schemes.includes(scheme)) return;
37
+ schemes.push(scheme);
38
+ data.LSApplicationQueriesSchemes = schemes;
39
+ }
40
+
41
+ export function addUrlScheme(data: Record<string, unknown>, urlScheme: string): void {
42
+ if (!urlScheme) return;
43
+ const types = Array.isArray(data.CFBundleURLTypes)
44
+ ? [...(data.CFBundleURLTypes as Array<Record<string, unknown>>)]
45
+ : [];
46
+ for (const entry of types) {
47
+ const urls = entry.CFBundleURLSchemes as string[] | undefined;
48
+ if (urls?.includes(urlScheme)) return;
49
+ }
50
+ types.push({
51
+ CFBundleTypeRole: "Editor",
52
+ CFBundleURLSchemes: [urlScheme],
53
+ });
54
+ data.CFBundleURLTypes = types;
55
+ }
@@ -0,0 +1,516 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import type { StepResult, WorkspaceContext } from "../contracts/types.js";
5
+ import {
6
+ MEETSDK_REMOTE_CONFIG_FILENAME,
7
+ tryParseAsMeetSdkRemoteConfig,
8
+ validateMeetSdkRemoteConfig,
9
+ } from "../config/meetSdkRemoteConfig.js";
10
+ import type { TextFileStore } from "../ops/fileStore.js";
11
+ import type { BinaryCopy } from "../ops/handlers.js";
12
+ import { buildChannelConfigMap, enabledIosPluginFolders, unsupportedIosModuleKeys } from "./channelConfig.js";
13
+ import { CodeUtils, findDelegateFiles, findSceneDelegateFiles } from "./codeUtils.js";
14
+ import type { CodeConfig, LoadedPluginConfig } from "./types.js";
15
+ import { DEFAULT_PERFORM_SETTINGS, type PerformSetting } from "./types.js";
16
+ import { TopSdkFileManager } from "./fileManager.js";
17
+ import {
18
+ addPlistParam,
19
+ addQueriesScheme,
20
+ addUrlScheme,
21
+ buildPlistXml,
22
+ parsePlistXml,
23
+ setAppTransportSecurity,
24
+ type PlistDocument,
25
+ } from "./infoPlist.js";
26
+ import {
27
+ addCopyFile,
28
+ addOtherLinkerFlag,
29
+ addSourceOrResourceFile,
30
+ addSystemFramework,
31
+ addSystemLib,
32
+ addThirdPartyFramework,
33
+ findInfoPlistPathsFromPbx,
34
+ loadPbxFromStore,
35
+ savePbxToStore,
36
+ setBuildSetting,
37
+ type PbxContext,
38
+ } from "./pbxprojEditor.js";
39
+ import { findPluginByFolder, listSdkCoreConfigs, validateLoadedPluginResources } from "./pluginConfig.js";
40
+ import { resolveIosSdkRoot } from "./sdkBundle.js";
41
+ import { applyChannelTemplate, applyChannelTemplateValue } from "./template.js";
42
+ import { ensureAppleSignInEntitlement } from "./entitlements.js";
43
+
44
+ export interface IosIntegrateOptions {
45
+ dryRun?: boolean;
46
+ targetName?: string;
47
+ executeAppDelegate?: boolean;
48
+ performSettings?: readonly PerformSetting[];
49
+ }
50
+
51
+ const IOS_FIREBASE_CONFIG_FILE = "GoogleService-Info.plist";
52
+
53
+ function ok(changed: string[], warnings: string[] = []): StepResult {
54
+ return { ok: true, changedFiles: changed, warnings, errors: [] };
55
+ }
56
+
57
+ function fail(errors: string[], warnings: string[] = []): StepResult {
58
+ return { ok: false, changedFiles: [], warnings, errors };
59
+ }
60
+
61
+ function applyPlugin(
62
+ loaded: LoadedPluginConfig,
63
+ pbx: PbxContext,
64
+ fm: TopSdkFileManager,
65
+ channelConfig: Record<string, unknown>,
66
+ perform: readonly PerformSetting[],
67
+ binaryCopies: BinaryCopy[],
68
+ dryRun: boolean,
69
+ remoteSources: Map<string, string> = new Map()
70
+ ): void {
71
+ const { config, sourceDir } = loaded;
72
+ const srcRoot = pbx.srcRoot;
73
+
74
+ if (perform.includes("frameworks") && config.frameworks?.length) {
75
+ for (const fw of config.frameworks) {
76
+ if (fw.system) {
77
+ addSystemFramework(pbx, fw.name);
78
+ continue;
79
+ }
80
+ const src = path.join(sourceDir, fw.name);
81
+ if (!fs.existsSync(src)) throw new Error(`missing framework resource for ${config.name}: ${fw.name}`);
82
+ const relTo = path.join(fm.topSdkRel, path.basename(src)).split(path.sep).join("/");
83
+ if (dryRun) {
84
+ binaryCopies.push({ fromAbs: src, relTo });
85
+ } else {
86
+ fm.copyFramework(src);
87
+ }
88
+ addThirdPartyFramework(pbx, relTo, Boolean(fw.embed));
89
+ if (fw.copyFile) addCopyFile(pbx, relTo);
90
+ }
91
+ }
92
+
93
+ if (perform.includes("libs") && config.libs?.length) {
94
+ for (const lib of config.libs) {
95
+ if (lib.system) {
96
+ addSystemLib(pbx, lib.name);
97
+ continue;
98
+ }
99
+ const src = path.join(sourceDir, lib.name);
100
+ if (!fs.existsSync(src)) throw new Error(`missing lib resource for ${config.name}: ${lib.name}`);
101
+ const relTo = path.join(fm.topSdkRel, path.basename(src)).split(path.sep).join("/");
102
+ if (dryRun) binaryCopies.push({ fromAbs: src, relTo });
103
+ else fm.copyFile(src);
104
+ addSourceOrResourceFile(pbx, relTo);
105
+ }
106
+ }
107
+
108
+ if (perform.includes("sources") && config.sources?.length) {
109
+ for (const source of config.sources) {
110
+ const remoteRel = remoteSources.get(`${config.name}:${source}`);
111
+ if (remoteRel) {
112
+ addSourceOrResourceFile(pbx, remoteRel);
113
+ continue;
114
+ }
115
+ const src = path.join(sourceDir, source);
116
+ if (!fs.existsSync(src)) throw new Error(`missing source resource for ${config.name}: ${source}`);
117
+ if (
118
+ fs.statSync(src).isDirectory() &&
119
+ !src.endsWith(".bundle") &&
120
+ !src.endsWith(".framework") &&
121
+ !src.endsWith(".xcdatamodeld")
122
+ ) {
123
+ if (!dryRun) {
124
+ const copied = fm.copyDir(src);
125
+ const walk = (dir: string): void => {
126
+ for (const name of fs.readdirSync(dir)) {
127
+ const full = path.join(dir, name);
128
+ if (fs.statSync(full).isDirectory()) {
129
+ if (name.endsWith(".bundle")) {
130
+ addSourceOrResourceFile(pbx, fm.relFromSrc(full));
131
+ } else {
132
+ walk(full);
133
+ }
134
+ continue;
135
+ }
136
+ if (/\.(h|m|mm|plist|a)$/.test(name)) {
137
+ addSourceOrResourceFile(pbx, fm.relFromSrc(full));
138
+ }
139
+ }
140
+ };
141
+ walk(copied);
142
+ }
143
+ } else {
144
+ const relTo = path.join(fm.topSdkRel, path.basename(src)).split(path.sep).join("/");
145
+ if (dryRun) binaryCopies.push({ fromAbs: src, relTo });
146
+ else if (fs.statSync(src).isDirectory()) fm.copyDir(src);
147
+ else fm.copyFile(src);
148
+ addSourceOrResourceFile(pbx, relTo);
149
+ }
150
+ }
151
+ }
152
+
153
+ if (perform.includes("buildSettings") && config.buildSettings) {
154
+ for (const [k, v] of Object.entries(config.buildSettings)) {
155
+ setBuildSetting(pbx, k, v);
156
+ }
157
+ }
158
+
159
+ if (perform.includes("OtherLinkerFlags") && config.OtherLinkerFlags?.length) {
160
+ for (const flag of config.OtherLinkerFlags) {
161
+ addOtherLinkerFlag(pbx, flag);
162
+ }
163
+ }
164
+
165
+ }
166
+
167
+ function validateLoadedPluginResourcesForIos(loaded: LoadedPluginConfig): string[] {
168
+ const errors = validateLoadedPluginResources(loaded);
169
+ if (loaded.config.name !== "FirebaseManager") return errors;
170
+ return errors.filter((error) => !error.includes(`missing source resource for FirebaseManager: ${IOS_FIREBASE_CONFIG_FILE}`));
171
+ }
172
+
173
+ async function downloadIosFirebaseConfig(
174
+ remote: ReturnType<typeof tryParseAsMeetSdkRemoteConfig>,
175
+ pbx: PbxContext,
176
+ dryRun: boolean,
177
+ relTo: string
178
+ ): Promise<{ remoteSources: Map<string, string>; warnings: string[]; errors: string[] }> {
179
+ const remoteSources = new Map<string, string>();
180
+ const warnings: string[] = [];
181
+ const errors: string[] = [];
182
+ const firebase = remote?.sdkModules.analytics.firebase;
183
+ if (typeof firebase !== "object" || firebase === null) {
184
+ return { remoteSources, warnings, errors };
185
+ }
186
+
187
+ const url = firebase.firebase_file_url?.trim();
188
+ if (!url) {
189
+ errors.push("iOS Firebase config requires sdkModules.analytics.firebase.firebaseUrl");
190
+ return { remoteSources, warnings, errors };
191
+ }
192
+
193
+ remoteSources.set(`FirebaseManager:${IOS_FIREBASE_CONFIG_FILE}`, relTo);
194
+
195
+ if (dryRun) {
196
+ warnings.push(`would download ${IOS_FIREBASE_CONFIG_FILE} to ${relTo}`);
197
+ return { remoteSources, warnings, errors };
198
+ }
199
+
200
+ let res: Response;
201
+ try {
202
+ res = await fetch(url);
203
+ } catch (e) {
204
+ const msg = e instanceof Error ? e.message : String(e);
205
+ errors.push(`iOS Firebase config download failed: ${msg}`);
206
+ return { remoteSources, warnings, errors };
207
+ }
208
+ if (!res.ok) {
209
+ errors.push(`iOS Firebase config download failed: HTTP ${res.status} ${res.statusText}`);
210
+ return { remoteSources, warnings, errors };
211
+ }
212
+
213
+ const abs = path.join(pbx.srcRoot, relTo);
214
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
215
+ fs.writeFileSync(abs, Buffer.from(await res.arrayBuffer()));
216
+ warnings.push(`downloaded ${IOS_FIREBASE_CONFIG_FILE} to ${relTo}`);
217
+ return { remoteSources, warnings, errors };
218
+ }
219
+
220
+ function iosFirebaseConfigRelPath(plistDocs: PlistDocument[]): string {
221
+ const relPlist = plistDocs[0]?.relPath;
222
+ if (!relPlist) return IOS_FIREBASE_CONFIG_FILE;
223
+ const dir = path.posix.dirname(relPlist.replace(/\\/g, "/"));
224
+ return dir === "." ? IOS_FIREBASE_CONFIG_FILE : path.posix.join(dir, IOS_FIREBASE_CONFIG_FILE);
225
+ }
226
+
227
+ function appDelegateCodeShouldMoveToSceneDelegate(code: CodeConfig): boolean {
228
+ if (code.type === "header") return false;
229
+ const method = code.method ?? "";
230
+ return (
231
+ method.includes("openURL") ||
232
+ method.includes("continueUserActivity") ||
233
+ method.includes("applicationDidEnterBackground") ||
234
+ method.includes("applicationWillEnterForeground")
235
+ );
236
+ }
237
+
238
+ function appDelegateCodesForLifecycle(loaded: LoadedPluginConfig, hasUniqueSceneDelegate: boolean): CodeConfig[] {
239
+ const codes = loaded.config.appDelegateCodes ?? [];
240
+ if (!hasUniqueSceneDelegate) return codes;
241
+ return codes.filter((code) => !appDelegateCodeShouldMoveToSceneDelegate(code));
242
+ }
243
+
244
+ function readDottedPath(obj: Record<string, unknown>, dottedPath: string): unknown {
245
+ let cur: unknown = obj;
246
+ for (const part of dottedPath.split(".")) {
247
+ if (!part) continue;
248
+ if (cur && typeof cur === "object" && part in (cur as Record<string, unknown>)) {
249
+ cur = (cur as Record<string, unknown>)[part];
250
+ } else {
251
+ return undefined;
252
+ }
253
+ }
254
+ return cur;
255
+ }
256
+
257
+ function validateRequiredChannelConfigs(
258
+ loadedConfigs: LoadedPluginConfig[],
259
+ channelConfig: Record<string, unknown>
260
+ ): string[] {
261
+ const missing: string[] = [];
262
+ for (const loaded of loadedConfigs) {
263
+ for (const required of loaded.config.requireConfigs ?? []) {
264
+ const value = readDottedPath(channelConfig, required);
265
+ if (value === undefined || value === null || String(value).length === 0) {
266
+ missing.push(`${loaded.config.name}: ${required}`);
267
+ }
268
+ }
269
+ }
270
+ return missing;
271
+ }
272
+
273
+ function mergePlistParams(
274
+ docs: PlistDocument[],
275
+ loaded: LoadedPluginConfig,
276
+ channelConfig: Record<string, unknown>,
277
+ perform: readonly PerformSetting[]
278
+ ): void {
279
+ if (!perform.includes("infoParams") && !perform.includes("urlScheme") && !perform.includes("queriesSchemes")) {
280
+ return;
281
+ }
282
+ for (const doc of docs) {
283
+ if (perform.includes("infoParams") && loaded.config.infoParams) {
284
+ for (const [key, raw] of Object.entries(loaded.config.infoParams)) {
285
+ addPlistParam(doc.data, key, applyChannelTemplateValue(raw, channelConfig));
286
+ }
287
+ }
288
+ if (perform.includes("queriesSchemes") && loaded.config.queriesSchemes?.length) {
289
+ for (const scheme of loaded.config.queriesSchemes) {
290
+ addQueriesScheme(doc.data, scheme);
291
+ }
292
+ }
293
+ if (perform.includes("urlScheme") && loaded.config.urlScheme) {
294
+ let scheme = applyChannelTemplate(loaded.config.urlScheme, channelConfig);
295
+ if (scheme.endsWith("://")) scheme = scheme.replace("://", "");
296
+ addUrlScheme(doc.data, scheme);
297
+ }
298
+ }
299
+ }
300
+
301
+ function buildTopSdkPluginInfo(
302
+ plugins: LoadedPluginConfig[],
303
+ channelConfig: Record<string, unknown>
304
+ ): { pluginsInfo: Array<Record<string, unknown>>; dataPluginsInfo: Array<Record<string, unknown>> } {
305
+ const pluginsInfo: Array<Record<string, unknown>> = [];
306
+ const dataPluginsInfo: Array<Record<string, unknown>> = [];
307
+ for (const loaded of plugins) {
308
+ const info: Record<string, unknown> = { name: loaded.config.name };
309
+ const key = loaded.config.key;
310
+ if (key && Object.prototype.hasOwnProperty.call(channelConfig, key)) {
311
+ const params: Record<string, string> = {};
312
+ for (const [paramKey, rawValue] of Object.entries(loaded.config.pluginParams ?? {})) {
313
+ params[paramKey] = applyChannelTemplate(String(rawValue), channelConfig);
314
+ }
315
+ info.params = params;
316
+ }
317
+ if (loaded.config.type === 4) {
318
+ dataPluginsInfo.push(info);
319
+ } else {
320
+ pluginsInfo.push(info);
321
+ }
322
+ }
323
+ return { pluginsInfo, dataPluginsInfo };
324
+ }
325
+
326
+ export async function runIosIntegrateTopSdk(
327
+ ctx: WorkspaceContext,
328
+ store: TextFileStore,
329
+ binaryCopies: BinaryCopy[],
330
+ options: IosIntegrateOptions = {}
331
+ ): Promise<StepResult> {
332
+ if (!ctx.ios?.ok) return fail(["iOS project not detected"]);
333
+ const warnings: string[] = [];
334
+ const changed = new Set<string>();
335
+
336
+ const configPath = path.join(ctx.projectRoot, MEETSDK_REMOTE_CONFIG_FILENAME);
337
+ if (!fs.existsSync(configPath)) {
338
+ return fail([`${MEETSDK_REMOTE_CONFIG_FILENAME} not found; run fetch-config or setup first`]);
339
+ }
340
+ let remote: ReturnType<typeof tryParseAsMeetSdkRemoteConfig>;
341
+ try {
342
+ remote = tryParseAsMeetSdkRemoteConfig(JSON.parse(fs.readFileSync(configPath, "utf8")) as unknown);
343
+ } catch (e) {
344
+ return fail([e instanceof Error ? e.message : String(e)]);
345
+ }
346
+ if (!remote) {
347
+ return fail([`invalid ${MEETSDK_REMOTE_CONFIG_FILENAME}`]);
348
+ }
349
+ const validation = validateMeetSdkRemoteConfig(remote);
350
+ if (!validation.ok) {
351
+ warnings.push(`remote config missing fields: ${validation.missing.join(", ")}`);
352
+ }
353
+
354
+ let sdkRoot: string;
355
+ try {
356
+ sdkRoot = resolveIosSdkRoot(ctx.packageRoot);
357
+ } catch (e) {
358
+ return fail([e instanceof Error ? e.message : String(e)]);
359
+ }
360
+
361
+ const targetName = options.targetName ?? ctx.ios.targetName ?? path.basename(ctx.ios.xcodeprojPath!, ".xcodeproj");
362
+ const perform = options.performSettings ?? DEFAULT_PERFORM_SETTINGS;
363
+ const executeAppDelegate = options.executeAppDelegate !== false;
364
+ const dryRun = Boolean(options.dryRun);
365
+
366
+ const channelConfig = buildChannelConfigMap(remote);
367
+ const coreConfigs = listSdkCoreConfigs(sdkRoot);
368
+ const pluginFolders = enabledIosPluginFolders(remote);
369
+ for (const key of unsupportedIosModuleKeys(remote)) {
370
+ warnings.push(`iOS plugin mapping not found for sdkModules.${key}; skipped`);
371
+ }
372
+ const pluginConfigs: LoadedPluginConfig[] = [];
373
+ for (const folder of pluginFolders) {
374
+ const p = findPluginByFolder(sdkRoot, folder);
375
+ if (p) pluginConfigs.push(p);
376
+ else warnings.push(`iOS plugin folder not in SDK package: plugins/${folder}`);
377
+ }
378
+ const xcodeprojPath = ctx.ios.xcodeprojPath!;
379
+ const pbx = await loadPbxFromStore(store, ctx.projectRoot, xcodeprojPath, targetName);
380
+ const fm = new TopSdkFileManager(pbx.srcRoot);
381
+
382
+ let plistDocs: PlistDocument[];
383
+ try {
384
+ plistDocs = await ensureInfoPlists(store, ctx, pbx, targetName);
385
+ } catch (e) {
386
+ return fail([e instanceof Error ? e.message : String(e)], warnings);
387
+ }
388
+
389
+ const firebaseDownload = await downloadIosFirebaseConfig(remote, pbx, dryRun, iosFirebaseConfigRelPath(plistDocs));
390
+ warnings.push(...firebaseDownload.warnings);
391
+ if (firebaseDownload.errors.length) return fail(firebaseDownload.errors, warnings);
392
+
393
+ const resourceErrors = [...coreConfigs, ...pluginConfigs].flatMap(validateLoadedPluginResourcesForIos);
394
+ if (resourceErrors.length) return fail(resourceErrors, warnings);
395
+
396
+ const missingRequiredConfigs = validateRequiredChannelConfigs(pluginConfigs, channelConfig);
397
+ if (missingRequiredConfigs.length) {
398
+ return fail([`iOS remote config missing required channel params: ${missingRequiredConfigs.join(", ")}`], warnings);
399
+ }
400
+
401
+ for (const loaded of coreConfigs) {
402
+ applyPlugin(loaded, pbx, fm, channelConfig, perform, binaryCopies, dryRun, firebaseDownload.remoteSources);
403
+ }
404
+ for (const loaded of pluginConfigs) {
405
+ applyPlugin(loaded, pbx, fm, channelConfig, perform, binaryCopies, dryRun, firebaseDownload.remoteSources);
406
+ }
407
+
408
+ for (const loaded of [...coreConfigs, ...pluginConfigs]) {
409
+ mergePlistParams(plistDocs, loaded, channelConfig, perform);
410
+ if (perform.includes("infoParams")) {
411
+ setAppTransportSecurity(plistDocs[0]?.data ?? {}, true);
412
+ }
413
+ }
414
+ for (const doc of plistDocs) {
415
+ if (perform.includes("infoParams")) {
416
+ const { pluginsInfo, dataPluginsInfo } = buildTopSdkPluginInfo(pluginConfigs, channelConfig);
417
+ addPlistParam(doc.data, "TOPSDK", {
418
+ APP_ID: remote.topsdk.appId,
419
+ Plugins: pluginsInfo,
420
+ dataPlugins: dataPluginsInfo,
421
+ });
422
+ }
423
+ const rel = path.relative(ctx.projectRoot, path.join(pbx.srcRoot, doc.relPath)).split(path.sep).join("/");
424
+ store.write(rel, buildPlistXml(doc.data));
425
+ changed.add(rel);
426
+ }
427
+
428
+ if (perform.includes("infoParams") && pluginConfigs.some((p) => p.config.name === "AppleSignin")) {
429
+ for (const rel of ensureAppleSignInEntitlement(store, ctx.projectRoot, pbx)) {
430
+ changed.add(rel);
431
+ }
432
+ }
433
+
434
+ savePbxToStore(store, pbx);
435
+ changed.add(pbx.rel);
436
+
437
+ if (executeAppDelegate) {
438
+ const sceneDelegateRelPaths: string[] = [];
439
+ for (const abs of findSceneDelegateFiles(pbx.srcRoot)) {
440
+ sceneDelegateRelPaths.push(path.relative(ctx.projectRoot, abs).split(path.sep).join("/"));
441
+ }
442
+ const hasUniqueSceneDelegate = sceneDelegateRelPaths.length === 1;
443
+
444
+ const delegateRelPaths: string[] = [];
445
+ for (const abs of findDelegateFiles(pbx.srcRoot)) {
446
+ delegateRelPaths.push(path.relative(ctx.projectRoot, abs).split(path.sep).join("/"));
447
+ }
448
+ if (!delegateRelPaths.length) {
449
+ warnings.push("no UIApplicationDelegate .m/.mm found; skipped AppDelegate injection (SwiftUI @main is not supported yet)");
450
+ }
451
+ if (delegateRelPaths.length > 1) {
452
+ warnings.push(`multiple UIApplicationDelegate files found; skipped AppDelegate injection: ${delegateRelPaths.join(", ")}`);
453
+ }
454
+ for (const rel of delegateRelPaths.length === 1 ? delegateRelPaths : []) {
455
+ const cu = CodeUtils.fromFile(path.join(ctx.projectRoot, rel));
456
+ let okInject = true;
457
+ for (const loaded of [...coreConfigs, ...pluginConfigs]) {
458
+ for (const code of appDelegateCodesForLifecycle(loaded, hasUniqueSceneDelegate)) {
459
+ if (code.type === "header") {
460
+ if (!cu.addHeader(code.content)) okInject = false;
461
+ } else if ((code.type === "method" || code.type === "code") && code.method) {
462
+ if (!cu.addCodeToMethod(code.method, code.content, Boolean(code.addToReturn))) okInject = false;
463
+ }
464
+ }
465
+ }
466
+ if (!okInject) warnings.push(`AppDelegate injection incomplete for ${rel}`);
467
+ const before = store.read(rel);
468
+ cu.applyToStore((c) => store.write(rel, c));
469
+ if (store.read(rel) !== before) changed.add(rel);
470
+ }
471
+
472
+ if (sceneDelegateRelPaths.length > 1) {
473
+ warnings.push(`multiple UIWindowSceneDelegate files found; skipped SceneDelegate injection: ${sceneDelegateRelPaths.join(", ")}`);
474
+ }
475
+ for (const rel of sceneDelegateRelPaths.length === 1 ? sceneDelegateRelPaths : []) {
476
+ const cu = CodeUtils.fromFile(path.join(ctx.projectRoot, rel));
477
+ let okInject = true;
478
+ for (const loaded of [...coreConfigs, ...pluginConfigs]) {
479
+ for (const code of loaded.config.sceneDelegateCodes ?? []) {
480
+ if (code.type === "header") {
481
+ if (!cu.addHeader(code.content)) okInject = false;
482
+ } else if ((code.type === "method" || code.type === "code") && code.method) {
483
+ if (!cu.addCodeToMethod(code.method, code.content, Boolean(code.addToReturn))) okInject = false;
484
+ }
485
+ }
486
+ }
487
+ if (!okInject) warnings.push(`SceneDelegate injection incomplete for ${rel}`);
488
+ const before = store.read(rel);
489
+ cu.applyToStore((c) => store.write(rel, c));
490
+ if (store.read(rel) !== before) changed.add(rel);
491
+ }
492
+ }
493
+
494
+ return ok([...changed], warnings);
495
+ }
496
+
497
+ async function ensureInfoPlists(
498
+ store: TextFileStore,
499
+ ctx: WorkspaceContext,
500
+ pbx: PbxContext,
501
+ targetName: string
502
+ ): Promise<PlistDocument[]> {
503
+ const absPaths = findInfoPlistPathsFromPbx(pbx);
504
+ if (absPaths.length) {
505
+ return absPaths.map((abs) => {
506
+ const rel = path.relative(ctx.projectRoot, abs).split(path.sep).join("/");
507
+ if (!fs.existsSync(abs)) {
508
+ throw new Error(`Info.plist not found for iOS target ${targetName}: ${rel}`);
509
+ }
510
+ const xml = store.read(rel) || fs.readFileSync(abs, "utf8");
511
+ return { relPath: path.relative(pbx.srcRoot, abs).split(path.sep).join("/"), data: parsePlistXml(xml) };
512
+ });
513
+ }
514
+
515
+ throw new Error(`Info.plist not found for iOS target ${targetName}; configure INFOPLIST_FILE before running setup`);
516
+ }