@leonxin/meetgames 0.1.8 → 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 (285) 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/cli.d.ts.map +1 -1
  47. package/dist/cli.js +157 -31
  48. package/dist/cli.js.map +1 -1
  49. package/dist/config/meetSdkDefaultConfig.d.ts +19 -2
  50. package/dist/config/meetSdkDefaultConfig.d.ts.map +1 -1
  51. package/dist/config/meetSdkDefaultConfig.js +67 -5
  52. package/dist/config/meetSdkDefaultConfig.js.map +1 -1
  53. package/dist/config/meetSdkIosConfig.d.ts +21 -0
  54. package/dist/config/meetSdkIosConfig.d.ts.map +1 -0
  55. package/dist/config/meetSdkIosConfig.js +66 -0
  56. package/dist/config/meetSdkIosConfig.js.map +1 -0
  57. package/dist/config/meetSdkRemoteConfig.d.ts +18 -1
  58. package/dist/config/meetSdkRemoteConfig.d.ts.map +1 -1
  59. package/dist/config/meetSdkRemoteConfig.js +61 -25
  60. package/dist/config/meetSdkRemoteConfig.js.map +1 -1
  61. package/dist/contracts/types.d.ts +19 -6
  62. package/dist/contracts/types.d.ts.map +1 -1
  63. package/dist/core/doctor.d.ts +17 -0
  64. package/dist/core/doctor.d.ts.map +1 -0
  65. package/dist/core/doctor.js +444 -0
  66. package/dist/core/doctor.js.map +1 -0
  67. package/dist/core/pipeline.d.ts.map +1 -1
  68. package/dist/core/pipeline.js +0 -15
  69. package/dist/core/pipeline.js.map +1 -1
  70. package/dist/core/platform.d.ts +12 -0
  71. package/dist/core/platform.d.ts.map +1 -0
  72. package/dist/core/platform.js +40 -0
  73. package/dist/core/platform.js.map +1 -0
  74. package/dist/core/reporter.js +1 -1
  75. package/dist/core/reporter.js.map +1 -1
  76. package/dist/core/workspace.d.ts +2 -2
  77. package/dist/core/workspace.d.ts.map +1 -1
  78. package/dist/core/workspace.js +4 -5
  79. package/dist/core/workspace.js.map +1 -1
  80. package/dist/index.d.ts +3 -1
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.js +3 -1
  83. package/dist/index.js.map +1 -1
  84. package/dist/ios/channelConfig.d.ts +1 -0
  85. package/dist/ios/channelConfig.d.ts.map +1 -1
  86. package/dist/ios/channelConfig.js +82 -0
  87. package/dist/ios/channelConfig.js.map +1 -1
  88. package/dist/ios/codeUtils.d.ts +1 -0
  89. package/dist/ios/codeUtils.d.ts.map +1 -1
  90. package/dist/ios/codeUtils.js +11 -2
  91. package/dist/ios/codeUtils.js.map +1 -1
  92. package/dist/ios/detect.d.ts +2 -2
  93. package/dist/ios/detect.d.ts.map +1 -1
  94. package/dist/ios/detect.js +49 -10
  95. package/dist/ios/detect.js.map +1 -1
  96. package/dist/ios/entitlements.d.ts +4 -0
  97. package/dist/ios/entitlements.d.ts.map +1 -0
  98. package/dist/ios/entitlements.js +53 -0
  99. package/dist/ios/entitlements.js.map +1 -0
  100. package/dist/ios/fileManager.d.ts.map +1 -1
  101. package/dist/ios/fileManager.js +3 -2
  102. package/dist/ios/fileManager.js.map +1 -1
  103. package/dist/ios/infoPlist.d.ts +1 -1
  104. package/dist/ios/infoPlist.d.ts.map +1 -1
  105. package/dist/ios/infoPlist.js.map +1 -1
  106. package/dist/ios/integrate.d.ts.map +1 -1
  107. package/dist/ios/integrate.js +211 -36
  108. package/dist/ios/integrate.js.map +1 -1
  109. package/dist/ios/pbxprojEditor.d.ts +2 -0
  110. package/dist/ios/pbxprojEditor.d.ts.map +1 -1
  111. package/dist/ios/pbxprojEditor.js +179 -1
  112. package/dist/ios/pbxprojEditor.js.map +1 -1
  113. package/dist/ios/pluginConfig.d.ts +1 -0
  114. package/dist/ios/pluginConfig.d.ts.map +1 -1
  115. package/dist/ios/pluginConfig.js +36 -4
  116. package/dist/ios/pluginConfig.js.map +1 -1
  117. package/dist/ios/sdkBundle.d.ts +1 -1
  118. package/dist/ios/sdkBundle.d.ts.map +1 -1
  119. package/dist/ios/sdkBundle.js +7 -5
  120. package/dist/ios/sdkBundle.js.map +1 -1
  121. package/dist/ios/template.d.ts +1 -0
  122. package/dist/ios/template.d.ts.map +1 -1
  123. package/dist/ios/template.js +14 -1
  124. package/dist/ios/template.js.map +1 -1
  125. package/dist/ios/types.d.ts +2 -2
  126. package/dist/ios/types.d.ts.map +1 -1
  127. package/dist/mcp/server.d.ts.map +1 -1
  128. package/dist/mcp/server.js +14 -13
  129. package/dist/mcp/server.js.map +1 -1
  130. package/dist/mcp/service.d.ts +8 -6
  131. package/dist/mcp/service.d.ts.map +1 -1
  132. package/dist/mcp/service.js +34 -14
  133. package/dist/mcp/service.js.map +1 -1
  134. package/dist/ops/handlers.d.ts.map +1 -1
  135. package/dist/ops/handlers.js +10 -4
  136. package/dist/ops/handlers.js.map +1 -1
  137. package/dist/remote/sdkHomeDownload.d.ts +65 -0
  138. package/dist/remote/sdkHomeDownload.d.ts.map +1 -0
  139. package/dist/remote/sdkHomeDownload.js +208 -0
  140. package/dist/remote/sdkHomeDownload.js.map +1 -0
  141. package/dist/remote/topsdkDownloadSdkConfig.d.ts.map +1 -1
  142. package/dist/remote/topsdkDownloadSdkConfig.js +11 -1
  143. package/dist/remote/topsdkDownloadSdkConfig.js.map +1 -1
  144. package/dist/shared/errors.d.ts +7 -0
  145. package/dist/shared/errors.d.ts.map +1 -0
  146. package/dist/shared/errors.js +16 -0
  147. package/dist/shared/errors.js.map +1 -0
  148. package/dist/shared/fileUtils.d.ts +5 -0
  149. package/dist/shared/fileUtils.d.ts.map +1 -0
  150. package/dist/shared/fileUtils.js +35 -0
  151. package/dist/shared/fileUtils.js.map +1 -0
  152. package/dist/shared/logger.d.ts +10 -0
  153. package/dist/shared/logger.d.ts.map +1 -0
  154. package/dist/shared/logger.js +37 -0
  155. package/dist/shared/logger.js.map +1 -0
  156. package/dist/shared/pathUtils.d.ts +4 -0
  157. package/dist/shared/pathUtils.d.ts.map +1 -0
  158. package/dist/shared/pathUtils.js +22 -0
  159. package/dist/shared/pathUtils.js.map +1 -0
  160. package/dist/shared/processRunner.d.ts +12 -0
  161. package/dist/shared/processRunner.d.ts.map +1 -0
  162. package/dist/shared/processRunner.js +31 -0
  163. package/dist/shared/processRunner.js.map +1 -0
  164. package/docs/AAB_CONVERTER_CLI_PLAN.md +392 -0
  165. package/docs/API.md +246 -32
  166. package/docs/CLI.md +292 -0
  167. package/docs/INTEGRATION.md +116 -0
  168. package/docs/MCP.md +86 -0
  169. package/docs/README.md +19 -10
  170. package/docs/{api → archive/api}/downloadSDKConfig.md +8 -6
  171. package/docs/{api → archive/api}/getChannelConfig-meetgames.md +1 -1
  172. package/docs/archive/ios-migration.md +2139 -0
  173. 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
  174. package/docs/{ → archive/product/}/351/234/200/346/261/202/346/226/207/346/241/243.md +15 -14
  175. package/logs/convert-20260622-155037.log +5 -0
  176. package/logs/convert-20260622-155226.log +6 -0
  177. package/meetsdk-android.json +2 -1
  178. package/meetsdk-ios.json +15 -0
  179. package/package.json +10 -36
  180. package/scripts/package-aab-cli-win.mjs +193 -0
  181. package/src/aab-converter/aab-entry.ts +48 -0
  182. package/src/aab-converter/apksExtractor.ts +119 -0
  183. package/src/aab-converter/bundletoolRunner.ts +63 -0
  184. package/src/aab-converter/cliArgs.ts +194 -0
  185. package/src/aab-converter/convertAabToApk.ts +81 -0
  186. package/src/aab-converter/resourcePaths.ts +43 -0
  187. package/src/aab-converter/signingOptions.ts +29 -0
  188. package/src/aab-converter/types.ts +26 -0
  189. package/src/android/adapter.ts +9 -0
  190. package/src/android/assembleIntegrationJson.ts +33 -0
  191. package/src/android/detect.ts +132 -0
  192. package/src/android/downloadGoogleServicesJson.ts +56 -0
  193. package/src/android/gradle.ts +116 -0
  194. package/src/android/manifest.ts +50 -0
  195. package/src/android/meetSdkRemoteGradle.ts +837 -0
  196. package/src/cli.ts +488 -0
  197. package/src/config/fetchConfigWrite.ts +30 -0
  198. package/src/config/loadAndroidIntegration.ts +41 -0
  199. package/src/config/loadManifest.ts +40 -0
  200. package/src/config/meetSdkDefaultConfig.ts +99 -0
  201. package/src/config/meetSdkIosConfig.ts +87 -0
  202. package/src/config/meetSdkRemoteConfig.ts +1211 -0
  203. package/src/config/topsdkFeatureModules.ts +92 -0
  204. package/src/contracts/types.ts +121 -0
  205. package/src/core/doctor.ts +485 -0
  206. package/src/core/patch.ts +64 -0
  207. package/src/core/pipeline.ts +107 -0
  208. package/src/core/platform.ts +47 -0
  209. package/src/core/previewPatches.ts +23 -0
  210. package/src/core/reporter.ts +24 -0
  211. package/src/core/workspace.ts +29 -0
  212. package/src/entry.ts +7 -0
  213. package/src/index.ts +133 -0
  214. package/src/ios/channelConfig.ts +128 -0
  215. package/src/ios/codeUtils.ts +160 -0
  216. package/src/ios/detect.ts +105 -0
  217. package/src/ios/entitlements.ts +61 -0
  218. package/src/ios/fileManager.ts +48 -0
  219. package/src/ios/infoPlist.ts +55 -0
  220. package/src/ios/integrate.ts +516 -0
  221. package/src/ios/pbxprojEditor.ts +383 -0
  222. package/src/ios/pluginConfig.ts +97 -0
  223. package/src/ios/reserved.ts +8 -0
  224. package/src/ios/sdkBundle.ts +36 -0
  225. package/src/ios/template.ts +36 -0
  226. package/src/ios/types.ts +65 -0
  227. package/src/mcp/server.ts +170 -0
  228. package/src/mcp/service.ts +222 -0
  229. package/src/mcp-entry.ts +7 -0
  230. package/src/ops/fileStore.ts +56 -0
  231. package/src/ops/handlers.ts +304 -0
  232. package/src/remote/fetchJson.ts +22 -0
  233. package/src/remote/sdkHomeDownload.ts +274 -0
  234. package/src/remote/topsdkDownloadSdkConfig.ts +93 -0
  235. package/src/remote/topsdkGetSdkConfig.ts +122 -0
  236. package/src/remote/topsdkSign.ts +10 -0
  237. package/src/shared/errors.ts +16 -0
  238. package/src/shared/fileUtils.ts +41 -0
  239. package/src/shared/logger.ts +49 -0
  240. package/src/shared/pathUtils.ts +24 -0
  241. package/src/shared/processRunner.ts +43 -0
  242. package/test-projects/README.md +51 -0
  243. package/test-projects/_preview/pipeline.patch +281 -0
  244. package/tests/aab-converter.test.ts +213 -0
  245. package/tests/assemble.test.ts +12 -0
  246. package/tests/doctor.test.ts +89 -0
  247. package/tests/downloadGoogleServicesJson.test.ts +47 -0
  248. package/tests/fetch-remote.test.ts +23 -0
  249. package/tests/fetchConfigOverrides.test.ts +28 -0
  250. package/tests/fetchConfigWrite.test.ts +54 -0
  251. package/tests/gradle.test.ts +33 -0
  252. package/tests/integration-json.test.ts +29 -0
  253. package/tests/ios.codeUtils.test.ts +23 -0
  254. package/tests/ios.sdkBundle.test.ts +16 -0
  255. package/tests/loadManifest.test.ts +15 -0
  256. package/tests/manifest-xml.test.ts +30 -0
  257. package/tests/mcp.e2e.ts +217 -0
  258. package/tests/mcp.service.test.ts +53 -0
  259. package/tests/meetSdkRemoteConfig.test.ts +456 -0
  260. package/tests/meetSdkRemoteGradle.test.ts +414 -0
  261. package/tests/pipeline.android.test.ts +96 -0
  262. package/tests/pipeline.integration-json.test.ts +58 -0
  263. package/tests/pipeline.ios.test.ts +385 -0
  264. package/tests/pipeline.preview.patch.test.ts +85 -0
  265. package/tests/platformSelection.test.ts +77 -0
  266. package/tests/sdkHomeDownload.test.ts +124 -0
  267. package/tests/sdkVersionConfig.test.ts +130 -0
  268. package/tests/test-projects-hosts.test.ts +78 -0
  269. package/tests/topsdk.test.ts +53 -0
  270. package/tests/topsdkDownloadSdkConfig.test.ts +81 -0
  271. package/tests/topsdkFeatureModules.test.ts +116 -0
  272. package/tsconfig.json +19 -0
  273. package/vitest.config.ts +9 -0
  274. package/vitest.mcp.config.ts +11 -0
  275. package/bundled/android/sample.txt +0 -1
  276. package/docs/ANDROID.md +0 -133
  277. package/docs/CURSOR-MCP-SETUP.md +0 -72
  278. package/docs/MCP-SKILL.md +0 -63
  279. package/fixtures/api-samples/getChannelConfig-meetgames.sample.json +0 -123
  280. package/fixtures/meetsdk-remote-config.download-shape.json +0 -20
  281. package/fixtures/meetsdk-remote-config.mock.json +0 -69
  282. package/fixtures/recipes/android-default.fixture.yaml +0 -15
  283. package/fixtures/recipes/android-integration.fixture.json +0 -29
  284. package/fixtures/topsdk-config-reference.json +0 -39
  285. /package/docs/{api → archive/api}/getSDKConfig.md +0 -0
@@ -0,0 +1,837 @@
1
+ /**
2
+ * Gradle edits from `meetsdk-remote-config.json`, aligned with sdk-integration-agent
3
+ * `src/lib/gradleEditor.js` (TOPSDK marker blocks).
4
+ */
5
+ import type { MeetSdkGradlePluginDslSpec, MeetSdkRemoteConfig } from "../config/meetSdkRemoteConfig.js";
6
+ import { buildTopSdkFeatureImplementationLines } from "../config/topsdkFeatureModules.js";
7
+ import { findBlockRange, replaceOrInsertManaged } from "./gradle.js";
8
+
9
+ /** Same markers as sdk-integration-agent / topsdk-agent */
10
+ export const TOPSDK_AUTO_START = "// >>> TOPSDK AUTO START";
11
+ export const TOPSDK_AUTO_END = "// >>> TOPSDK AUTO END";
12
+ export const TOPSDK_REPO_AUTO_START = "// >>> TOPSDK REPO AUTO START";
13
+ export const TOPSDK_REPO_AUTO_END = "// >>> TOPSDK REPO AUTO END";
14
+ export const TOPSDK_PLUGIN_AUTO_START = "// >>> TOPSDK PLUGIN AUTO START";
15
+ export const TOPSDK_PLUGIN_AUTO_END = "// >>> TOPSDK PLUGIN AUTO END";
16
+
17
+ /** resValue keys managed by TOPSDK (includes legacy keys removed on re-integrate). */
18
+ const TOPSDK_MANAGED_RESVALUE_KEYS = [
19
+ "top_channel_id",
20
+ "top_app_id",
21
+ "facebook_app_id",
22
+ "fb_login_protocol_scheme",
23
+ "facebook_client_token",
24
+ "google_client_id",
25
+ "twitter_client_id",
26
+ "twitter_client_secret",
27
+ "twitter_redirect_uri",
28
+ "twitter_redirect_url",
29
+ "snapchat_client_id",
30
+ "snapchat_redirect_uri",
31
+ "snapchat_client_secret",
32
+ "tiktok_client_id",
33
+ "tiktok_client_key",
34
+ "tiktok_client_secret",
35
+ "tiktok_redirect_uri",
36
+ "discord_client_id",
37
+ "discord_client_secret",
38
+ "discord_redirect_uri",
39
+ "line_channel_id",
40
+ "kakao_app_id",
41
+ "kakao_scheme",
42
+ "kakao_native_app_key",
43
+ "kakao_uri_scheme",
44
+ "naver_client_id",
45
+ "naver_client_secret",
46
+ "naver_client_name",
47
+ "appsflyer_enable_debug_log",
48
+ "af_dev_key",
49
+ "adjust_enable_sandbox",
50
+ "adjust_app_token",
51
+ "adjust_app_id",
52
+ "facebook_data_app_id",
53
+ "facebook_data_client_token",
54
+ "firebase_google_services_json_url",
55
+ ] as const;
56
+
57
+ /** Escape for values embedded in Gradle single-quoted strings */
58
+ export function escapeGradleSingleQuotedString(value: string): string {
59
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
60
+ }
61
+
62
+ /** For Groovy `"${...}"` string templates in def assignments */
63
+ export function escapeGroovyDoubleQuotedInner(value: string): string {
64
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
65
+ }
66
+
67
+ const RES_VALUE_KEY_RE = /resValue\s*\(\s*'[^']+'\s*,\s*'([^']+)'/;
68
+ const IMPLEMENTATION_COORD_RE = /implementation\s+["']([^"']+)["']/;
69
+ const APPLY_PLUGIN_ID_RE = /apply\s+plugin:\s*['"]([^'"]+)['"]/;
70
+ const PLUGINS_DSL_ID_RE = /^\s*id\s+(?:\(\s*)?['"]([^'"]+)['"](?:\s*\))?/;
71
+ const CLASSPATH_COORD_RE = /classpath\s+['"]([^'"]+)['"]/;
72
+ const DEF_GROUP_ID_RE = /def\s+groupId\s*=\s*["']([^"']+)["']/;
73
+
74
+ function fullBlockTextMerge(
75
+ blockText: string,
76
+ startMarker: string,
77
+ endMarker: string,
78
+ snippet: string
79
+ ): string {
80
+ return replaceOrInsertManaged(
81
+ blockText,
82
+ { start: 0, openBrace: 0, end: blockText.length },
83
+ startMarker,
84
+ endMarker,
85
+ snippet
86
+ );
87
+ }
88
+
89
+ function lineDeclaresRepository(line: string, repo: string): boolean {
90
+ const t = line.trim();
91
+ if (repo === "google") return /\bgoogle\s*\(\s*\)/.test(t);
92
+ if (repo === "mavenCentral") return /\bmavenCentral\s*\(\s*\)/.test(t);
93
+ if (!t.includes("maven")) return false;
94
+ return t.includes(repo);
95
+ }
96
+
97
+ export function extractResValueKey(line: string): string | null {
98
+ const m = line.trim().match(RES_VALUE_KEY_RE);
99
+ return m?.[1] ?? null;
100
+ }
101
+
102
+ /** Maven coordinate key for upsert: `group:artifact` (version ignored). */
103
+ export function mavenGroupArtifactKey(coordinate: string): string {
104
+ const parts = coordinate.split(":");
105
+ return parts.length >= 2 ? `${parts[0]}:${parts[1]}` : coordinate;
106
+ }
107
+
108
+ /** @deprecated Use {@link mavenGroupArtifactKey}. */
109
+ export const classpathCoordinateKey = mavenGroupArtifactKey;
110
+
111
+ export function extractImplementationCoordContent(line: string): string | null {
112
+ const m = line.trim().match(IMPLEMENTATION_COORD_RE);
113
+ return m?.[1] ?? null;
114
+ }
115
+
116
+ function resolveGroupIdFromBlock(blockText: string, fallbackGroupId: string): string {
117
+ const m = blockText.match(DEF_GROUP_ID_RE);
118
+ return m?.[1] ?? fallbackGroupId;
119
+ }
120
+
121
+ /** Resolve `group:artifact` from implementation coord (`$groupId:ui:…` or literal `com.sino.topsdk:ui:1.0`). */
122
+ export function implementationGroupArtifactKey(
123
+ coordContent: string,
124
+ blockText: string,
125
+ fallbackGroupId: string
126
+ ): string | null {
127
+ const parts = coordContent.split(":");
128
+ if (parts.length < 2) return null;
129
+ let group = parts[0].trim();
130
+ const artifact = parts[1].trim();
131
+ if (!artifact || artifact.includes("$")) return null;
132
+ if (group === "$groupId") {
133
+ group = resolveGroupIdFromBlock(blockText, fallbackGroupId);
134
+ }
135
+ if (!group || group.includes("$")) return null;
136
+ return `${group}:${artifact}`;
137
+ }
138
+
139
+ export function extractImplementationKey(
140
+ line: string,
141
+ blockText: string,
142
+ fallbackGroupId: string
143
+ ): string | null {
144
+ const content = extractImplementationCoordContent(line);
145
+ if (!content) return null;
146
+ return implementationGroupArtifactKey(content, blockText, fallbackGroupId);
147
+ }
148
+
149
+ /** Remove resValue lines whose resource name (2nd arg) is in `keys`. */
150
+ export function removeResValueLinesByKeys(blockText: string, keys: ReadonlySet<string>): string {
151
+ return blockText
152
+ .split("\n")
153
+ .filter((line) => {
154
+ const key = extractResValueKey(line);
155
+ return !key || !keys.has(key);
156
+ })
157
+ .join("\n");
158
+ }
159
+
160
+ export function extractApplyPluginId(line: string): string | null {
161
+ const m = line.trim().match(APPLY_PLUGIN_ID_RE);
162
+ return m?.[1] ?? null;
163
+ }
164
+
165
+ export function extractPluginsDslId(line: string): string | null {
166
+ const m = line.trim().match(PLUGINS_DSL_ID_RE);
167
+ return m?.[1] ?? null;
168
+ }
169
+
170
+ export type ModuleGradlePluginStyle = "plugins-dsl" | "apply-plugin";
171
+
172
+ /**
173
+ * Module uses `plugins { }` for the Android application plugin (no `apply plugin: com.android.application`).
174
+ */
175
+ export function detectModuleGradlePluginStyle(content: string): ModuleGradlePluginStyle {
176
+ if (/apply\s+plugin:\s*['"]com\.android\.application['"]/m.test(content)) {
177
+ return "apply-plugin";
178
+ }
179
+ const pluginsBlock = findBlockRange(content, "plugins");
180
+ const androidBlock = findBlockRange(content, "android");
181
+ if (!pluginsBlock || (androidBlock && pluginsBlock.start > androidBlock.start)) {
182
+ return "apply-plugin";
183
+ }
184
+ const blockText = content.slice(pluginsBlock.openBrace + 1, pluginsBlock.end);
185
+ if (
186
+ /alias\s*\(\s*libs\.plugins\.android\.application\s*\)/.test(blockText) ||
187
+ /\bid\s+['"]com\.android\.application['"]/.test(blockText) ||
188
+ /\bid\s*\(\s*['"]com\.android\.application['"]\s*\)/.test(blockText)
189
+ ) {
190
+ return "plugins-dsl";
191
+ }
192
+ return "apply-plugin";
193
+ }
194
+
195
+ /** Remove `plugins { id '…' }` lines for the same plugin id (managed block or not). */
196
+ export function removePluginsDslLinesByIds(text: string, pluginIds: ReadonlySet<string>): string {
197
+ return text
198
+ .split("\n")
199
+ .filter((line) => {
200
+ const id = extractPluginsDslId(line);
201
+ return !id || !pluginIds.has(id);
202
+ })
203
+ .join("\n");
204
+ }
205
+
206
+ export function extractClasspathCoordinate(line: string): string | null {
207
+ const m = line.trim().match(CLASSPATH_COORD_RE);
208
+ return m?.[1] ?? null;
209
+ }
210
+
211
+ /** Remove `apply plugin` lines for the same plugin id (managed block or not). */
212
+ export function removeApplyPluginLinesByIds(text: string, pluginIds: ReadonlySet<string>): string {
213
+ return text
214
+ .split("\n")
215
+ .filter((line) => {
216
+ const id = extractApplyPluginId(line);
217
+ return !id || !pluginIds.has(id);
218
+ })
219
+ .join("\n");
220
+ }
221
+
222
+ /** Remove `classpath` lines with the same group:artifact as any target coordinate. */
223
+ export function removeClasspathLinesByCoordinates(text: string, coordinates: ReadonlySet<string>): string {
224
+ const keys = new Set([...coordinates].map(mavenGroupArtifactKey));
225
+ return text
226
+ .split("\n")
227
+ .filter((line) => {
228
+ const coord = extractClasspathCoordinate(line);
229
+ if (!coord) return true;
230
+ return !keys.has(mavenGroupArtifactKey(coord));
231
+ })
232
+ .join("\n");
233
+ }
234
+
235
+ /** Remove repository declarations that match keys in `repositories` (google / mavenCentral / URL). */
236
+ export function removeRepositoryLinesByKeys(blockText: string, repositories: readonly string[]): string {
237
+ if (!repositories.length) return blockText;
238
+ return blockText
239
+ .split("\n")
240
+ .filter((line) => !repositories.some((repo) => lineDeclaresRepository(line, repo)))
241
+ .join("\n");
242
+ }
243
+
244
+ /**
245
+ * Upsert repository declarations: drop existing lines for the same repo keys, then write TOPSDK REPO block.
246
+ */
247
+ function repositoryToGradleLine(repo: string): string {
248
+ if (repo === "google") return " google()";
249
+ if (repo === "mavenCentral") return " mavenCentral()";
250
+ return ` maven { url '${escapeGradleSingleQuotedString(repo)}' }`;
251
+ }
252
+
253
+ export function mergeRepositoriesInBlock(blockText: string, repositories: readonly string[]): string {
254
+ let cleaned = removeRepositoryLinesByKeys(blockText, repositories);
255
+ cleaned = stripManagedBlocks(cleaned, TOPSDK_REPO_AUTO_START, TOPSDK_REPO_AUTO_END);
256
+ if (!repositories.length) return cleaned;
257
+ const snippet = [
258
+ TOPSDK_REPO_AUTO_START,
259
+ ...repositories.map(repositoryToGradleLine),
260
+ TOPSDK_REPO_AUTO_END,
261
+ ].join("\n");
262
+ return fullBlockTextMerge(cleaned, TOPSDK_REPO_AUTO_START, TOPSDK_REPO_AUTO_END, snippet);
263
+ }
264
+
265
+ /**
266
+ * Upsert buildscript classpaths: drop existing lines with same coordinate, then write TOPSDK AUTO block.
267
+ */
268
+ export function mergeClasspathsInBlock(blockText: string, classpaths: readonly string[]): string {
269
+ const coords = new Set(classpaths);
270
+ let cleaned = removeClasspathLinesByCoordinates(blockText, coords);
271
+ cleaned = stripManagedBlocks(cleaned, TOPSDK_AUTO_START, TOPSDK_AUTO_END);
272
+ if (!classpaths.length) return cleaned;
273
+ return fullBlockTextMerge(cleaned, TOPSDK_AUTO_START, TOPSDK_AUTO_END, buildTopSdkBuildscriptClasspathSnippet(classpaths));
274
+ }
275
+
276
+ /**
277
+ * Insert PLUGIN block immediately after the leading `apply plugin:` lines (e.g. right below
278
+ * `com.android.application`), not before `android { }` / `dependencies { }`.
279
+ */
280
+ function insertAfterLeadingApplyPlugins(content: string, snippet: string): string {
281
+ const eol = content.includes("\r\n") ? "\r\n" : "\n";
282
+ const lines = content.split(/\r?\n/);
283
+ let lastApplyIdx = -1;
284
+ for (let i = 0; i < lines.length; i++) {
285
+ const trimmed = lines[i].trim();
286
+ if (/^apply\s+plugin\s*:/.test(trimmed)) {
287
+ lastApplyIdx = i;
288
+ continue;
289
+ }
290
+ if (lastApplyIdx >= 0) {
291
+ if (trimmed === "") continue;
292
+ break;
293
+ }
294
+ }
295
+ if (lastApplyIdx < 0) {
296
+ return replaceOrInsertTopLevelManaged(
297
+ content,
298
+ TOPSDK_PLUGIN_AUTO_START,
299
+ TOPSDK_PLUGIN_AUTO_END,
300
+ snippet,
301
+ "android"
302
+ );
303
+ }
304
+ const before = lines.slice(0, lastApplyIdx + 1).join(eol);
305
+ const after = lines.slice(lastApplyIdx + 1).join(eol);
306
+ if (!after) return `${before}${eol}${eol}${snippet}${eol}`;
307
+ return `${before}${eol}${eol}${snippet}${eol}${after}`;
308
+ }
309
+
310
+ /**
311
+ * Upsert module-level apply plugins on the full file: drop duplicate plugin ids, then write PLUGIN block.
312
+ */
313
+ export function mergeApplyPluginsInContent(content: string, applyPlugins: readonly string[]): string {
314
+ if (!applyPlugins.length) return content;
315
+ const ids = new Set(applyPlugins);
316
+ let cleaned = removeApplyPluginLinesByIds(content, ids);
317
+ cleaned = stripManagedBlocks(cleaned, TOPSDK_PLUGIN_AUTO_START, TOPSDK_PLUGIN_AUTO_END);
318
+ return insertAfterLeadingApplyPlugins(cleaned, buildApplyPluginsSnippet(applyPlugins));
319
+ }
320
+
321
+ /** Remove implementation lines with the same `group:artifact` as any target key. */
322
+ export function removeImplementationLinesByGroupArtifact(
323
+ blockText: string,
324
+ keys: ReadonlySet<string>,
325
+ fallbackGroupId: string
326
+ ): string {
327
+ return blockText
328
+ .split("\n")
329
+ .filter((line) => {
330
+ const key = extractImplementationKey(line, blockText, fallbackGroupId);
331
+ return !key || !keys.has(key);
332
+ })
333
+ .join("\n");
334
+ }
335
+
336
+ /**
337
+ * Before inserting TOPSDK managed resValue block: drop any existing lines with the same
338
+ * resValue keys (inside or outside markers), then insert/update the managed block.
339
+ */
340
+ export function mergeResValuesInDefaultConfigBlock(blockText: string, managedSnippet: string): string {
341
+ const desiredLines = managedSnippet
342
+ .split("\n")
343
+ .map((l) => l.trimEnd())
344
+ .filter((l) => l.includes("resValue("));
345
+ const keys = new Set(
346
+ [...desiredLines.map((l) => extractResValueKey(l)).filter((k): k is string => Boolean(k)), ...TOPSDK_MANAGED_RESVALUE_KEYS]
347
+ );
348
+ let cleaned = removeResValueLinesByKeys(blockText, keys);
349
+ cleaned = stripManagedBlocks(cleaned, TOPSDK_AUTO_START, TOPSDK_AUTO_END);
350
+ return replaceOrInsertManaged(
351
+ cleaned,
352
+ { start: 0, openBrace: 0, end: cleaned.length },
353
+ TOPSDK_AUTO_START,
354
+ TOPSDK_AUTO_END,
355
+ managedSnippet
356
+ );
357
+ }
358
+
359
+ function mergeDependenciesInBlock(
360
+ blockText: string,
361
+ managedSnippet: string,
362
+ fallbackGroupId: string
363
+ ): string {
364
+ const lookupText = `${blockText}\n${managedSnippet}`;
365
+ const desiredLines = managedSnippet
366
+ .split("\n")
367
+ .map((l) => l.trimEnd())
368
+ .filter((l) => l.includes("implementation "));
369
+ const keys = new Set(
370
+ desiredLines
371
+ .map((l) => extractImplementationKey(l, lookupText, fallbackGroupId))
372
+ .filter((k): k is string => Boolean(k))
373
+ );
374
+ const alsoStrip = (line: string) =>
375
+ /^\s*def\s+topsdk_version\s*=/.test(line) || /^\s*def\s+groupId\s*=/.test(line);
376
+ let cleaned = blockText
377
+ .split("\n")
378
+ .filter((line) => !alsoStrip(line))
379
+ .join("\n");
380
+ cleaned = removeImplementationLinesByGroupArtifact(cleaned, keys, fallbackGroupId);
381
+ cleaned = stripManagedBlocks(cleaned, TOPSDK_AUTO_START, TOPSDK_AUTO_END);
382
+ return replaceOrInsertManaged(
383
+ cleaned,
384
+ { start: 0, openBrace: 0, end: cleaned.length },
385
+ TOPSDK_AUTO_START,
386
+ TOPSDK_AUTO_END,
387
+ managedSnippet
388
+ );
389
+ }
390
+
391
+ function stripManagedBlocks(text: string, startMarker: string, endMarker: string): string {
392
+ const start = text.indexOf(startMarker);
393
+ const end = text.indexOf(endMarker);
394
+ if (start >= 0 && end > start) {
395
+ return text.slice(0, start) + text.slice(end + endMarker.length);
396
+ }
397
+ return text;
398
+ }
399
+
400
+ function findNestedBlockRange(content: string, keywords: readonly string[]): { start: number; openBrace: number; end: number } | null {
401
+ let parentStart = 0;
402
+ let parentEnd = content.length;
403
+ let current: { start: number; openBrace: number; end: number } | null = null;
404
+ for (const keyword of keywords) {
405
+ current = findBlockRange(content, keyword, parentStart);
406
+ if (!current || current.end > parentEnd) return null;
407
+ parentStart = current.start;
408
+ parentEnd = current.end;
409
+ }
410
+ return current;
411
+ }
412
+
413
+ function buildTopSdkBuildscriptClasspathSnippet(classpaths: readonly string[]): string {
414
+ const lines = [TOPSDK_AUTO_START];
415
+ for (const classpath of classpaths) {
416
+ lines.push(` classpath '${escapeGradleSingleQuotedString(classpath)}'`);
417
+ }
418
+ lines.push(TOPSDK_AUTO_END);
419
+ return lines.join("\n");
420
+ }
421
+
422
+ function ensureBuildscriptBlock(content: string): string {
423
+ if (findBlockRange(content, "buildscript")) return content;
424
+ return `buildscript {\n repositories {\n }\n dependencies {\n }\n}\n\n${content}`;
425
+ }
426
+
427
+ export function updateRootBuildGradleMeetSdkRemote(
428
+ content: string,
429
+ params:
430
+ | readonly string[]
431
+ | {
432
+ repositories: readonly string[];
433
+ buildscriptRepositories?: readonly string[];
434
+ buildscriptClasspaths?: readonly string[];
435
+ rootPluginsDsl?: readonly MeetSdkGradlePluginDslSpec[];
436
+ stripPluginsDslIds?: readonly string[];
437
+ stripBuildscriptClasspaths?: readonly string[];
438
+ }
439
+ ): { ok: true; content: string; changed: boolean; warnings: string[] } | { ok: false; error: string } {
440
+ const warnings: string[] = [];
441
+ const options = Array.isArray(params)
442
+ ? {
443
+ repositories: params,
444
+ buildscriptRepositories: [] as readonly string[],
445
+ buildscriptClasspaths: [] as readonly string[],
446
+ rootPluginsDsl: [] as readonly MeetSdkGradlePluginDslSpec[],
447
+ stripPluginsDslIds: [] as readonly string[],
448
+ stripBuildscriptClasspaths: [] as readonly string[],
449
+ }
450
+ : (params as {
451
+ repositories: readonly string[];
452
+ buildscriptRepositories?: readonly string[];
453
+ buildscriptClasspaths?: readonly string[];
454
+ rootPluginsDsl?: readonly MeetSdkGradlePluginDslSpec[];
455
+ stripPluginsDslIds?: readonly string[];
456
+ stripBuildscriptClasspaths?: readonly string[];
457
+ });
458
+ const repositories = options.repositories;
459
+ const buildscriptRepositories = options.buildscriptRepositories ?? [];
460
+ const buildscriptClasspaths = options.buildscriptClasspaths ?? [];
461
+ const rootPluginsDsl = options.rootPluginsDsl ?? [];
462
+ const stripPluginsDslIds = options.stripPluginsDslIds ?? [];
463
+ const stripBuildscriptClasspathCoords = options.stripBuildscriptClasspaths ?? [];
464
+
465
+ let current = content;
466
+ if (rootPluginsDsl.length > 0) {
467
+ current = mergePluginsDslInContent(current, rootPluginsDsl);
468
+ if (stripBuildscriptClasspathCoords.length > 0) {
469
+ current = stripBuildscriptClasspaths(current, stripBuildscriptClasspathCoords);
470
+ }
471
+ }
472
+ if (stripPluginsDslIds.length > 0) {
473
+ current = removePluginsDslLinesByIds(current, new Set(stripPluginsDslIds));
474
+ current = stripManagedBlocks(current, TOPSDK_PLUGIN_AUTO_START, TOPSDK_PLUGIN_AUTO_END);
475
+ }
476
+ if (buildscriptRepositories.length > 0 || buildscriptClasspaths.length > 0) {
477
+ current = ensureBuildscriptBlock(current);
478
+ const buildscript = findBlockRange(current, "buildscript");
479
+ if (!buildscript) return { ok: false, error: "buildscript block not found in root build.gradle" };
480
+ const repositoriesBlock = findBlockRange(current, "repositories", buildscript.start);
481
+ if (!repositoriesBlock || repositoriesBlock.end > buildscript.end) {
482
+ return { ok: false, error: "repositories block under buildscript not found" };
483
+ }
484
+ const buildscriptReposText = current.slice(repositoriesBlock.openBrace + 1, repositoriesBlock.end);
485
+ const mergedBuildscriptRepos = mergeRepositoriesInBlock(buildscriptReposText, buildscriptRepositories);
486
+ current =
487
+ current.slice(0, repositoriesBlock.openBrace + 1) +
488
+ mergedBuildscriptRepos +
489
+ current.slice(repositoriesBlock.end);
490
+
491
+ const refreshedBuildscript = findBlockRange(current, "buildscript");
492
+ if (!refreshedBuildscript) return { ok: false, error: "buildscript block not found in root build.gradle" };
493
+ const dependenciesBlock = findBlockRange(current, "dependencies", refreshedBuildscript.start);
494
+ if (!dependenciesBlock || dependenciesBlock.end > refreshedBuildscript.end) {
495
+ return { ok: false, error: "dependencies block under buildscript not found" };
496
+ }
497
+ const buildscriptDepsText = current.slice(dependenciesBlock.openBrace + 1, dependenciesBlock.end);
498
+ const mergedClasspaths = mergeClasspathsInBlock(buildscriptDepsText, buildscriptClasspaths);
499
+ current =
500
+ current.slice(0, dependenciesBlock.openBrace + 1) +
501
+ mergedClasspaths +
502
+ current.slice(dependenciesBlock.end);
503
+ }
504
+
505
+ let updated = current;
506
+ if (repositories.length > 0) {
507
+ const allprojects = findBlockRange(current, "allprojects");
508
+ if (!allprojects) {
509
+ return { ok: false, error: "allprojects block not found in root build.gradle" };
510
+ }
511
+ const repositoriesBlock = findBlockRange(current, "repositories", allprojects.start);
512
+ if (!repositoriesBlock || repositoriesBlock.end > allprojects.end) {
513
+ return { ok: false, error: "repositories block under allprojects not found" };
514
+ }
515
+ const allprojectsReposText = current.slice(repositoriesBlock.openBrace + 1, repositoriesBlock.end);
516
+ const mergedAllprojectsRepos = mergeRepositoriesInBlock(allprojectsReposText, repositories);
517
+ updated =
518
+ current.slice(0, repositoriesBlock.openBrace + 1) +
519
+ mergedAllprojectsRepos +
520
+ current.slice(repositoriesBlock.end);
521
+ }
522
+ if (updated === content) warnings.push("root build.gradle TOPSDK repositories block unchanged");
523
+ return { ok: true, content: updated, changed: updated !== content, warnings };
524
+ }
525
+
526
+ export function updateSettingsGradleMeetSdkRemote(
527
+ content: string,
528
+ repositories: readonly string[]
529
+ ): { ok: true; content: string; changed: boolean; warnings: string[] } | { ok: false; error: string } {
530
+ const warnings: string[] = [];
531
+ const repositoriesBlock = findNestedBlockRange(content, ["dependencyResolutionManagement", "repositories"]);
532
+ if (!repositoriesBlock) {
533
+ return { ok: false, error: "dependencyResolutionManagement.repositories block not found in settings.gradle" };
534
+ }
535
+ const settingsReposText = content.slice(repositoriesBlock.openBrace + 1, repositoriesBlock.end);
536
+ const mergedSettingsRepos = mergeRepositoriesInBlock(settingsReposText, repositories);
537
+ const updated =
538
+ content.slice(0, repositoriesBlock.openBrace + 1) +
539
+ mergedSettingsRepos +
540
+ content.slice(repositoriesBlock.end);
541
+ if (updated === content) warnings.push("settings.gradle TOPSDK repositories block unchanged");
542
+ return { ok: true, content: updated, changed: updated !== content, warnings };
543
+ }
544
+
545
+ function buildApplyPluginsSnippet(applyPlugins: readonly string[]): string {
546
+ const lines = [TOPSDK_PLUGIN_AUTO_START];
547
+ for (const plugin of applyPlugins) {
548
+ lines.push(`apply plugin: '${escapeGradleSingleQuotedString(plugin)}'`);
549
+ }
550
+ lines.push(TOPSDK_PLUGIN_AUTO_END);
551
+ return lines.join("\n");
552
+ }
553
+
554
+ function buildPluginsDslSnippet(specs: readonly MeetSdkGradlePluginDslSpec[], indent = " "): string {
555
+ const lines = [`${indent}${TOPSDK_PLUGIN_AUTO_START}`];
556
+ for (const spec of specs) {
557
+ let line = `${indent}id '${escapeGradleSingleQuotedString(spec.id)}'`;
558
+ if (spec.version) line += ` version '${escapeGradleSingleQuotedString(spec.version)}'`;
559
+ if (spec.applyFalse) line += " apply false";
560
+ lines.push(line);
561
+ }
562
+ lines.push(`${indent}${TOPSDK_PLUGIN_AUTO_END}`);
563
+ return lines.join("\n");
564
+ }
565
+
566
+ function mergePluginsDslInBlock(blockText: string, specs: readonly MeetSdkGradlePluginDslSpec[]): string {
567
+ const ids = new Set(specs.map((s) => s.id));
568
+ let cleaned = removePluginsDslLinesByIds(blockText, ids);
569
+ cleaned = stripManagedBlocks(cleaned, TOPSDK_PLUGIN_AUTO_START, TOPSDK_PLUGIN_AUTO_END);
570
+ if (!specs.length) return cleaned;
571
+ return fullBlockTextMerge(cleaned, TOPSDK_PLUGIN_AUTO_START, TOPSDK_PLUGIN_AUTO_END, buildPluginsDslSnippet(specs));
572
+ }
573
+
574
+ /**
575
+ * Upsert module/root `plugins { id '…' }` entries inside the existing `plugins { }` block.
576
+ */
577
+ export function mergePluginsDslInContent(content: string, specs: readonly MeetSdkGradlePluginDslSpec[]): string {
578
+ if (!specs.length) return content;
579
+ const ids = new Set(specs.map((s) => s.id));
580
+ let cleaned = removePluginsDslLinesByIds(content, ids);
581
+ cleaned = removeApplyPluginLinesByIds(cleaned, ids);
582
+ cleaned = stripManagedBlocks(cleaned, TOPSDK_PLUGIN_AUTO_START, TOPSDK_PLUGIN_AUTO_END);
583
+ const pluginsBlock = findBlockRange(cleaned, "plugins");
584
+ if (!pluginsBlock) {
585
+ return `plugins {\n${buildPluginsDslSnippet(specs)}\n}\n\n${cleaned}`;
586
+ }
587
+ const blockText = cleaned.slice(pluginsBlock.openBrace + 1, pluginsBlock.end);
588
+ const merged = mergePluginsDslInBlock(blockText, specs);
589
+ return cleaned.slice(0, pluginsBlock.openBrace + 1) + merged + cleaned.slice(pluginsBlock.end);
590
+ }
591
+
592
+ function stripBuildscriptClasspaths(content: string, classpaths: readonly string[]): string {
593
+ if (!classpaths.length) return content;
594
+ const buildscript = findBlockRange(content, "buildscript");
595
+ if (!buildscript) return content;
596
+ const dependenciesBlock = findBlockRange(content, "dependencies", buildscript.start);
597
+ if (!dependenciesBlock || dependenciesBlock.end > buildscript.end) return content;
598
+ const depsText = content.slice(dependenciesBlock.openBrace + 1, dependenciesBlock.end);
599
+ const coords = new Set(classpaths);
600
+ let cleaned = removeClasspathLinesByCoordinates(depsText, coords);
601
+ cleaned = stripManagedBlocks(cleaned, TOPSDK_AUTO_START, TOPSDK_AUTO_END);
602
+ return (
603
+ content.slice(0, dependenciesBlock.openBrace + 1) + cleaned + content.slice(dependenciesBlock.end)
604
+ );
605
+ }
606
+
607
+ function replaceOrInsertTopLevelManaged(
608
+ content: string,
609
+ startMarker: string,
610
+ endMarker: string,
611
+ snippet: string,
612
+ insertBeforeKeyword: string
613
+ ): string {
614
+ const start = content.indexOf(startMarker);
615
+ const end = content.indexOf(endMarker);
616
+ if (start >= 0 && end > start) {
617
+ const before = content.slice(0, start).replace(/\s*$/, "\n");
618
+ const after = content.slice(end + endMarker.length).replace(/^\s*/, "\n");
619
+ return `${before}${snippet}${after}`;
620
+ }
621
+
622
+ const insertion = `${snippet}\n\n`;
623
+ const keywordPattern = new RegExp(`^\\s*${insertBeforeKeyword}\\b`, "m");
624
+ const match = keywordPattern.exec(content);
625
+ if (match?.index !== undefined) {
626
+ return content.slice(0, match.index).replace(/\s*$/, "\n\n") + insertion + content.slice(match.index);
627
+ }
628
+ return insertion + content;
629
+ }
630
+
631
+ function buildResValuesSnippet(config: MeetSdkRemoteConfig): string {
632
+ const lines = [
633
+ TOPSDK_AUTO_START,
634
+ ` resValue('string', 'top_channel_id', '${escapeGradleSingleQuotedString(config.channel)}')`,
635
+ ` resValue('string', 'top_app_id', '${escapeGradleSingleQuotedString(config.topsdk.appId)}')`,
636
+ ];
637
+ const loginFb = config.sdkModules.login.facebook;
638
+ if (typeof loginFb === "object" && loginFb !== null) {
639
+ lines.push(
640
+ ` resValue('string', 'facebook_app_id', '${escapeGradleSingleQuotedString(loginFb.facebookAppId)}')`,
641
+ ` resValue('string', 'fb_login_protocol_scheme', '${escapeGradleSingleQuotedString(loginFb.fbLoginProtocolScheme)}')`,
642
+ ` resValue('string', 'facebook_client_token', '${escapeGradleSingleQuotedString(loginFb.facebookClientToken)}')`
643
+ );
644
+ }
645
+ const loginGoogle = config.sdkModules.login.google;
646
+ if (typeof loginGoogle === "object" && loginGoogle !== null && loginGoogle.googleClientId) {
647
+ lines.push(
648
+ ` resValue('string', 'google_client_id', '${escapeGradleSingleQuotedString(loginGoogle.googleClientId)}')`
649
+ );
650
+ }
651
+ const loginTwitter = config.sdkModules.login.twitter;
652
+ if (typeof loginTwitter === "object" && loginTwitter !== null) {
653
+ lines.push(
654
+ ` resValue('string', 'twitter_client_id', '${escapeGradleSingleQuotedString(loginTwitter.clientId)}')`,
655
+ ` resValue('string', 'twitter_client_secret', '${escapeGradleSingleQuotedString(loginTwitter.secret)}')`,
656
+ ` resValue('string', 'twitter_redirect_url', '${escapeGradleSingleQuotedString(loginTwitter.redirect)}')`
657
+ );
658
+ }
659
+ const loginSnapchat = config.sdkModules.login.snapchat;
660
+ if (typeof loginSnapchat === "object" && loginSnapchat !== null) {
661
+ lines.push(
662
+ ` resValue('string', 'snapchat_client_id', '${escapeGradleSingleQuotedString(loginSnapchat.clientId)}')`,
663
+ ` resValue('string', 'snapchat_redirect_uri', '${escapeGradleSingleQuotedString(loginSnapchat.redirect)}')`
664
+ );
665
+ }
666
+ const loginTiktok = config.sdkModules.login.tiktok;
667
+ if (typeof loginTiktok === "object" && loginTiktok !== null) {
668
+ lines.push(
669
+ ` resValue('string', 'tiktok_client_id', '${escapeGradleSingleQuotedString(loginTiktok.clientId)}')`,
670
+ ` resValue('string', 'tiktok_client_secret', '${escapeGradleSingleQuotedString(loginTiktok.secret)}')`,
671
+ ` resValue('string', 'tiktok_redirect_uri', '${escapeGradleSingleQuotedString(loginTiktok.redirect)}')`
672
+ );
673
+ }
674
+ const loginDiscord = config.sdkModules.login.discord;
675
+ if (typeof loginDiscord === "object" && loginDiscord !== null) {
676
+ lines.push(
677
+ ` resValue('string', 'discord_client_id', '${escapeGradleSingleQuotedString(loginDiscord.clientId)}')`,
678
+ ` resValue('string', 'discord_client_secret', '${escapeGradleSingleQuotedString(loginDiscord.secret)}')`,
679
+ ` resValue('string', 'discord_redirect_uri', '${escapeGradleSingleQuotedString(loginDiscord.redirect)}')`
680
+ );
681
+ }
682
+ const loginLine = config.sdkModules.login.line;
683
+ if (typeof loginLine === "object" && loginLine !== null) {
684
+ lines.push(` resValue('string', 'line_channel_id', '${escapeGradleSingleQuotedString(loginLine.clientId)}')`);
685
+ }
686
+ const loginKakao = config.sdkModules.login.kakao;
687
+ if (typeof loginKakao === "object" && loginKakao !== null) {
688
+ lines.push(
689
+ ` resValue('string', 'kakao_app_id', '${escapeGradleSingleQuotedString(loginKakao.clientId)}')`,
690
+ ` resValue('string', 'kakao_scheme', '${escapeGradleSingleQuotedString(loginKakao.scheme)}')`
691
+ );
692
+ }
693
+ const loginNaver = config.sdkModules.login.naver;
694
+ if (typeof loginNaver === "object" && loginNaver !== null) {
695
+ lines.push(
696
+ ` resValue('string', 'naver_client_id', '${escapeGradleSingleQuotedString(loginNaver.clientId)}')`,
697
+ ` resValue('string', 'naver_client_secret', '${escapeGradleSingleQuotedString(loginNaver.secret)}')`,
698
+ ` resValue('string', 'naver_client_name', '${escapeGradleSingleQuotedString(loginNaver.name)}')`
699
+ );
700
+ }
701
+ const af = config.sdkModules.analytics.appsflyer;
702
+ if (typeof af === "object" && af !== null) {
703
+ lines.push(
704
+ ` resValue('bool', 'appsflyer_enable_debug_log', "${String(Boolean(af.enableDebugLog))}")`,
705
+ ` resValue('string', 'af_dev_key', '${escapeGradleSingleQuotedString(af.devKey)}')`
706
+ );
707
+ }
708
+ const analyticsAdjust = config.sdkModules.analytics.adjust;
709
+ if (typeof analyticsAdjust === "object" && analyticsAdjust !== null) {
710
+ lines.push(
711
+ ` resValue('bool', 'adjust_enable_sandbox', "${String(Boolean(analyticsAdjust.enableSandbox))}")`,
712
+ ` resValue('string', 'adjust_app_token', '${escapeGradleSingleQuotedString(analyticsAdjust.appId)}')`
713
+ );
714
+ }
715
+ const fbData = config.sdkModules.analytics.facebookdata;
716
+ if (typeof fbData === "object" && fbData !== null) {
717
+ const dataAppId = fbData.facebookAppId ?? (typeof loginFb === "object" && loginFb !== null ? loginFb.facebookAppId : "");
718
+ const dataClientToken =
719
+ fbData.facebookClientToken ?? (typeof loginFb === "object" && loginFb !== null ? loginFb.facebookClientToken : "");
720
+ if (dataAppId) {
721
+ lines.push(` resValue('string', 'facebook_data_app_id', '${escapeGradleSingleQuotedString(dataAppId)}')`);
722
+ }
723
+ if (dataClientToken) {
724
+ lines.push(
725
+ ` resValue('string', 'facebook_data_client_token', '${escapeGradleSingleQuotedString(dataClientToken)}')`
726
+ );
727
+ }
728
+ }
729
+ lines.push(TOPSDK_AUTO_END);
730
+ return lines.join("\n");
731
+ }
732
+
733
+ function buildRemoteDependenciesSnippet(config: MeetSdkRemoteConfig): string {
734
+ const lines = [
735
+ TOPSDK_AUTO_START,
736
+ ` def topsdk_version = "${escapeGroovyDoubleQuotedInner(config.topsdk.version)}"`,
737
+ ` def groupId = "${escapeGroovyDoubleQuotedInner(config.topsdk.groupId)}"`,
738
+ "",
739
+ ` implementation "$groupId:ui:$topsdk_version"`,
740
+ ...buildTopSdkFeatureImplementationLines(config),
741
+ ];
742
+ lines.push(TOPSDK_AUTO_END);
743
+ return lines.join("\n");
744
+ }
745
+
746
+ function updateDefaultConfigApplicationId(content: string, defaultConfigBlock: { openBrace: number; end: number }, applicationId: string): string {
747
+ const escaped = escapeGroovyDoubleQuotedInner(applicationId);
748
+ const blockText = content.slice(defaultConfigBlock.openBrace + 1, defaultConfigBlock.end);
749
+ const existing = /(^[ \t]*)applicationId\s+(?:=+\s*)?["'][^"']*["']/m;
750
+ if (existing.test(blockText)) {
751
+ const updatedBlockText = blockText.replace(existing, `$1applicationId "${escaped}"`);
752
+ return content.slice(0, defaultConfigBlock.openBrace + 1) + updatedBlockText + content.slice(defaultConfigBlock.end);
753
+ }
754
+
755
+ const newlineMatch = blockText.match(/\n([ \t]*)\S/);
756
+ const indent = newlineMatch ? newlineMatch[1] : " ";
757
+ const insertion = `\n${indent}applicationId "${escaped}"`;
758
+ return content.slice(0, defaultConfigBlock.openBrace + 1) + insertion + blockText + content.slice(defaultConfigBlock.end);
759
+ }
760
+
761
+ export type MeetSdkModulePluginConfig = {
762
+ style: ModuleGradlePluginStyle;
763
+ applyPlugins?: readonly string[];
764
+ pluginsDsl?: readonly MeetSdkGradlePluginDslSpec[];
765
+ };
766
+
767
+ export function updateModuleBuildGradleMeetSdkRemote(
768
+ content: string,
769
+ config: MeetSdkRemoteConfig,
770
+ plugins?: readonly string[] | MeetSdkModulePluginConfig
771
+ ): { ok: true; content: string; changed: boolean; warnings: string[] } | { ok: false; error: string } {
772
+ const warnings: string[] = [];
773
+ let pluginConfig: MeetSdkModulePluginConfig;
774
+ if (typeof plugins === "object" && plugins !== null && !Array.isArray(plugins) && "style" in plugins) {
775
+ pluginConfig = plugins;
776
+ } else {
777
+ pluginConfig = {
778
+ style: detectModuleGradlePluginStyle(content),
779
+ applyPlugins: Array.isArray(plugins) ? plugins : [],
780
+ pluginsDsl: [],
781
+ };
782
+ }
783
+ const withPlugins =
784
+ pluginConfig.style === "plugins-dsl" && (pluginConfig.pluginsDsl?.length ?? 0) > 0
785
+ ? mergePluginsDslInContent(content, pluginConfig.pluginsDsl!)
786
+ : (pluginConfig.applyPlugins?.length ?? 0) > 0
787
+ ? mergeApplyPluginsInContent(content, pluginConfig.applyPlugins!)
788
+ : content;
789
+
790
+ const androidBlock = findBlockRange(withPlugins, "android");
791
+ if (!androidBlock) {
792
+ return { ok: false, error: "android block not found in module build.gradle" };
793
+ }
794
+ const defaultConfigBlock = findBlockRange(withPlugins, "defaultConfig", androidBlock.start);
795
+ if (!defaultConfigBlock || defaultConfigBlock.end > androidBlock.end) {
796
+ return { ok: false, error: "defaultConfig block not found under android" };
797
+ }
798
+ const withApplicationId = updateDefaultConfigApplicationId(withPlugins, defaultConfigBlock, config.packageName);
799
+ const refreshedAndroidBlock = findBlockRange(withApplicationId, "android");
800
+ if (!refreshedAndroidBlock) {
801
+ return { ok: false, error: "android block not found in module build.gradle" };
802
+ }
803
+ const refreshedDefaultConfigBlock = findBlockRange(withApplicationId, "defaultConfig", refreshedAndroidBlock.start);
804
+ if (!refreshedDefaultConfigBlock || refreshedDefaultConfigBlock.end > refreshedAndroidBlock.end) {
805
+ return { ok: false, error: "defaultConfig block not found under android" };
806
+ }
807
+ const defaultConfigText = withApplicationId.slice(
808
+ refreshedDefaultConfigBlock.openBrace + 1,
809
+ refreshedDefaultConfigBlock.end
810
+ );
811
+ const mergedDefaultConfigText = mergeResValuesInDefaultConfigBlock(
812
+ defaultConfigText,
813
+ buildResValuesSnippet(config)
814
+ );
815
+ const withResValues =
816
+ withApplicationId.slice(0, refreshedDefaultConfigBlock.openBrace + 1) +
817
+ mergedDefaultConfigText +
818
+ withApplicationId.slice(refreshedDefaultConfigBlock.end);
819
+
820
+ const dependenciesBlock = findBlockRange(withResValues, "dependencies");
821
+ if (!dependenciesBlock) {
822
+ return { ok: false, error: "dependencies block not found in module build.gradle" };
823
+ }
824
+ const dependenciesText = withResValues.slice(dependenciesBlock.openBrace + 1, dependenciesBlock.end);
825
+ const mergedDependenciesText = mergeDependenciesInBlock(
826
+ dependenciesText,
827
+ buildRemoteDependenciesSnippet(config),
828
+ config.topsdk.groupId
829
+ );
830
+ const withDependencies =
831
+ withResValues.slice(0, dependenciesBlock.openBrace + 1) +
832
+ mergedDependenciesText +
833
+ withResValues.slice(dependenciesBlock.end);
834
+
835
+ if (withDependencies === content) warnings.push("module build.gradle TOPSDK blocks unchanged");
836
+ return { ok: true, content: withDependencies, changed: withDependencies !== content, warnings };
837
+ }