@cometchat/skills-cli 2.4.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -301,7 +301,8 @@ import { readdirSync } from "node:fs";
301
301
 
302
302
  // src/utils/version.ts
303
303
  function stripRange(versionRange) {
304
- return versionRange.replace(/^[\^~>=<\s]+/, "").trim();
304
+ const aliased = versionRange.replace(/^npm:.*@/, "");
305
+ return aliased.replace(/^[\^~>=<\s]+/, "").trim();
305
306
  }
306
307
  function extractMajor(versionRange) {
307
308
  if (!versionRange) return null;
@@ -362,15 +363,9 @@ function detectExpo(root, deps, pkg) {
362
363
  react_native_version: rnVersion
363
364
  };
364
365
  }
365
- function detectBareReactNative(root, deps) {
366
+ function detectBareReactNative(_root, deps) {
366
367
  if (!deps["react-native"]) return null;
367
368
  if (deps["expo"]) return null;
368
- const hasNativeIos = pathExists(p(root, "ios")) && (pathExists(p(root, "ios/Podfile")) || pathExists(p(root, "ios/Podfile.properties.json")));
369
- const hasNativeAndroid = pathExists(p(root, "android")) && pathExists(p(root, "android/build.gradle"));
370
- const hasRnConfig = pathExists(p(root, "react-native.config.js")) || pathExists(p(root, "react-native.config.ts"));
371
- if (!hasNativeIos && !hasNativeAndroid && !hasRnConfig) {
372
- return null;
373
- }
374
369
  const rnVersion = extractVersion(deps["react-native"]);
375
370
  return {
376
371
  framework: "react-native",
@@ -835,6 +830,207 @@ function detectExistingIntegration(root) {
835
830
  };
836
831
  }
837
832
 
833
+ // src/detectors/version-conflict.ts
834
+ init_fs();
835
+ function mergedDeps(root) {
836
+ const pkg = readJsonOrNull(p(root, "package.json"));
837
+ if (!pkg) return {};
838
+ return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
839
+ }
840
+ function matchMajor(text, re) {
841
+ const m = text.match(re);
842
+ return m && m[1] ? parseInt(m[1], 10) : null;
843
+ }
844
+ function androidBuildText(root) {
845
+ return ["build.gradle", "build.gradle.kts", "app/build.gradle", "app/build.gradle.kts"].map((f) => readFileOrNull(p(root, f)) ?? "").join("\n");
846
+ }
847
+ var WEB_FRAMEWORKS = /* @__PURE__ */ new Set(["reactjs", "nextjs", "react-router", "astro"]);
848
+ function detectVersionConflict(root, framework) {
849
+ const deps = mergedDeps(root);
850
+ if (framework && WEB_FRAMEWORKS.has(framework)) {
851
+ const m = extractMajor(deps["@cometchat/chat-uikit-react"]);
852
+ if (m != null && m !== 6) {
853
+ return {
854
+ kind: "kit-major-mismatch",
855
+ installed: `v${m}`,
856
+ expected: "v6",
857
+ detail: `Project has @cometchat/chat-uikit-react v${m} installed, but these skills target v6. Upgrade to ^6 (\`npm i @cometchat/chat-uikit-react@^6\`) before integrating \u2014 v${m} APIs differ from the v6 guidance the skills emit, so the generated code won't compile/run.`
858
+ };
859
+ }
860
+ }
861
+ if (framework === "react-native") {
862
+ const m = extractMajor(deps["@cometchat/chat-uikit-react-native"]);
863
+ if (m != null && m !== 5) {
864
+ return {
865
+ kind: "kit-major-mismatch",
866
+ installed: `v${m}`,
867
+ expected: "v5",
868
+ detail: `Project has @cometchat/chat-uikit-react-native v${m}, but these skills target v5 (the current RN UI Kit). Reconcile the installed version before integrating.`
869
+ };
870
+ }
871
+ }
872
+ if (framework === "angular") {
873
+ const m = extractMajor(deps["@cometchat/chat-uikit-angular"]);
874
+ if (m != null && m !== 5) {
875
+ return {
876
+ kind: "kit-major-mismatch",
877
+ installed: `v${m}`,
878
+ expected: "v5",
879
+ detail: `Project has @cometchat/chat-uikit-angular v${m}, but these skills target v5. Reconcile the installed version before integrating.`
880
+ };
881
+ }
882
+ }
883
+ if (framework === "android") {
884
+ const t = androidBuildText(root);
885
+ const hasV5 = /com\.cometchat:chat-uikit-android:5/.test(t);
886
+ const hasV6 = /com\.cometchat:chatuikit-(compose|kotlin)-android:6/.test(t);
887
+ if (hasV5 && hasV6) {
888
+ return {
889
+ kind: "both-versions",
890
+ installed: "V5 + V6",
891
+ detail: "Both the V5 UI Kit (com.cometchat:chat-uikit-android:5.x) and the V6 UI Kit (com.cometchat:chatuikit-{compose,kotlin}-android:6.x) are declared in the Gradle build. They are different SDKs with overlapping class names \u2192 duplicate-class / namespace-collision build failures. Remove the cohort you are NOT integrating before proceeding."
892
+ };
893
+ }
894
+ }
895
+ if (framework === "flutter") {
896
+ const pub = readFileOrNull(p(root, "pubspec.yaml")) ?? "";
897
+ const hasV6 = /cometchat_chat_uikit\s*:\s*[\^~]?6\./.test(pub);
898
+ const hasV5 = /cometchat_chat_uikit\s*:\s*[\^~]?5\./.test(pub) || /cometchat_calls_uikit\s*:\s*[\^~]?5\./.test(pub);
899
+ if (hasV6 && hasV5) {
900
+ return {
901
+ kind: "both-versions",
902
+ installed: "V5 + V6",
903
+ detail: "pubspec.yaml declares both V6 (cometchat_chat_uikit ^6.x) and V5 (cometchat_chat_uikit ^5.x / cometchat_calls_uikit ^5.x) CometChat packages. The V5 GetX kit and V6 Bloc kit cannot coexist. Remove the cohort you are NOT integrating before proceeding."
904
+ };
905
+ }
906
+ }
907
+ if (framework === "ios") {
908
+ const cocoapods = (readFileOrNull(p(root, "Podfile")) ?? "") + "\n" + (readFileOrNull(p(root, "Podfile.lock")) ?? "");
909
+ const spm = (readFileOrNull(p(root, "Package.swift")) ?? "") + "\n" + (readFileOrNull(p(root, "Package.resolved")) ?? "") + "\n" + (readFileOrNull(p(root, ".swiftpm/Package.resolved")) ?? "");
910
+ const major = matchMajor(cocoapods, /CometChatUIKitSwift[^,\n]*?[\s'"(](\d+)\./) ?? matchMajor(spm, /cometchatuikitswift[\s\S]{0,300}"version"\s*:\s*"(\d+)\./i) ?? matchMajor(spm, /CometChatUIKitSwift[\s\S]{0,200}from:\s*"(\d+)\./);
911
+ if (major != null && major !== 5) {
912
+ return {
913
+ kind: "kit-major-mismatch",
914
+ installed: `v${major}`,
915
+ expected: "v5",
916
+ detail: `Project pins CometChatUIKitSwift v${major}, but these skills target v5. Reconcile the pinned version before integrating.`
917
+ };
918
+ }
919
+ }
920
+ return null;
921
+ }
922
+
923
+ // src/detectors/coexistence.ts
924
+ init_fs();
925
+ function mergedDeps2(root) {
926
+ const pkg = readJsonOrNull(p(root, "package.json"));
927
+ if (!pkg) return {};
928
+ return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
929
+ }
930
+ var NPM_COMPETITORS = {
931
+ sendbird: { name: "Sendbird", kind: "chat" },
932
+ "@sendbird/chat": { name: "Sendbird", kind: "chat" },
933
+ "sendbird-uikit": { name: "Sendbird UIKit", kind: "chat" },
934
+ "@sendbird/uikit-react": { name: "Sendbird UIKit", kind: "chat" },
935
+ "@sendbird/uikit-react-native": { name: "Sendbird UIKit", kind: "chat" },
936
+ "@sendbird/calls": { name: "Sendbird Calls", kind: "calling" },
937
+ "stream-chat": { name: "Stream Chat", kind: "chat" },
938
+ "stream-chat-react": { name: "Stream Chat", kind: "chat" },
939
+ "stream-chat-react-native": { name: "Stream Chat", kind: "chat" },
940
+ "stream-chat-expo": { name: "Stream Chat", kind: "chat" },
941
+ "@stream-io/video-react-sdk": { name: "Stream Video", kind: "calling" },
942
+ "@stream-io/video-react-native-sdk": { name: "Stream Video", kind: "calling" },
943
+ "twilio-chat": { name: "Twilio Conversations", kind: "chat" },
944
+ "@twilio/conversations": { name: "Twilio Conversations", kind: "chat" },
945
+ "twilio-video": { name: "Twilio Video", kind: "calling" },
946
+ pubnub: { name: "PubNub", kind: "chat" },
947
+ "@pubnub/chat": { name: "PubNub Chat", kind: "chat" },
948
+ talkjs: { name: "TalkJS", kind: "chat" },
949
+ "@talkjs/react": { name: "TalkJS", kind: "chat" },
950
+ "agora-rtc-sdk-ng": { name: "Agora", kind: "calling" },
951
+ "agora-rtc-react": { name: "Agora", kind: "calling" },
952
+ "react-native-agora": { name: "Agora", kind: "calling" },
953
+ "react-native-onesignal": { name: "OneSignal", kind: "push" },
954
+ "onesignal-cordova-plugin": { name: "OneSignal", kind: "push" },
955
+ "react-onesignal": { name: "OneSignal", kind: "push" }
956
+ };
957
+ var NATIVE_COMPETITORS = [
958
+ // Android (Gradle coordinates) + Flutter (pub names) + iOS (pod names)
959
+ { pattern: /com\.sendbird\.sdk|sendbird_(sdk|chat_sdk)|\bSendBirdSDK\b|\bSendbirdChatSDK\b/, name: "Sendbird", kind: "chat" },
960
+ { pattern: /io\.getstream:stream-chat-android|stream_chat_flutter|\bStreamChat\b/, name: "Stream Chat", kind: "chat" },
961
+ { pattern: /io\.getstream:stream-video-android|stream_video_flutter|\bStreamVideo\b/, name: "Stream Video", kind: "calling" },
962
+ { pattern: /com\.twilio:conversations|twilio_conversations|\bTwilioConversationsClient\b/, name: "Twilio Conversations", kind: "chat" },
963
+ { pattern: /com\.twilio:video|twilio_programmable_video|\bTwilioVideo\b/, name: "Twilio Video", kind: "calling" },
964
+ { pattern: /com\.pubnub:pubnub|pubnub_flutter|(^|\n)\s*pod\s+['"]PubNub['"]/, name: "PubNub", kind: "chat" },
965
+ { pattern: /io\.agora\.rtc:|agora_rtc_engine|\bAgoraRtcEngine_iOS\b/, name: "Agora", kind: "calling" },
966
+ { pattern: /com\.onesignal:OneSignal|onesignal_flutter|(^|\n)\s*pod\s+['"]OneSignal/, name: "OneSignal", kind: "push" }
967
+ ];
968
+ function dedupeBy(items, key) {
969
+ const seen = /* @__PURE__ */ new Set();
970
+ const out = [];
971
+ for (const it of items) {
972
+ const k = key(it);
973
+ if (seen.has(k)) continue;
974
+ seen.add(k);
975
+ out.push(it);
976
+ }
977
+ return out;
978
+ }
979
+ function nativeManifestText(root) {
980
+ return [
981
+ "build.gradle",
982
+ "build.gradle.kts",
983
+ "app/build.gradle",
984
+ "app/build.gradle.kts",
985
+ "android/app/build.gradle",
986
+ // Flutter / RN-bare android
987
+ "pubspec.yaml",
988
+ "Podfile",
989
+ "ios/Podfile",
990
+ // Flutter / RN ios
991
+ "Package.swift"
992
+ ].map((f) => readFileOrNull(p(root, f)) ?? "").join("\n");
993
+ }
994
+ var SERVICE_WORKER_PATHS = [
995
+ "public/sw.js",
996
+ "public/service-worker.js",
997
+ "public/serviceworker.js",
998
+ "public/firebase-messaging-sw.js",
999
+ "static/sw.js",
1000
+ // Astro / SvelteKit static dir
1001
+ "static/service-worker.js",
1002
+ "src/service-worker.ts",
1003
+ "src/service-worker.js",
1004
+ "src/sw.ts",
1005
+ "service-worker.js",
1006
+ "sw.js"
1007
+ ];
1008
+ function detectExistingFirebase(root, deps) {
1009
+ if ("firebase" in deps || "@react-native-firebase/app" in deps || "@react-native-firebase/messaging" in deps) {
1010
+ return true;
1011
+ }
1012
+ return pathExists(p(root, "google-services.json")) || pathExists(p(root, "app/google-services.json")) || pathExists(p(root, "android/app/google-services.json")) || pathExists(p(root, "GoogleService-Info.plist")) || pathExists(p(root, "ios/GoogleService-Info.plist")) || pathExists(p(root, "ios/Runner/GoogleService-Info.plist")) || pathExists(p(root, "public/firebase-messaging-sw.js"));
1013
+ }
1014
+ function detectCoexistence(root, _framework) {
1015
+ const deps = mergedDeps2(root);
1016
+ const found = [];
1017
+ for (const [dep, meta] of Object.entries(NPM_COMPETITORS)) {
1018
+ if (dep in deps) found.push({ name: meta.name, coordinate: dep, kind: meta.kind });
1019
+ }
1020
+ const nativeText = nativeManifestText(root);
1021
+ if (nativeText.trim()) {
1022
+ for (const c of NATIVE_COMPETITORS) {
1023
+ if (c.pattern.test(nativeText)) {
1024
+ found.push({ name: c.name, coordinate: c.name, kind: c.kind });
1025
+ }
1026
+ }
1027
+ }
1028
+ const competing_sdks = dedupeBy(found, (s) => `${s.name}:${s.kind}`);
1029
+ const existing_firebase = detectExistingFirebase(root, deps);
1030
+ const existing_service_workers = SERVICE_WORKER_PATHS.filter((rel) => pathExists(p(root, rel)));
1031
+ return { competing_sdks, existing_firebase, existing_service_workers };
1032
+ }
1033
+
838
1034
  // src/detectors/compatibility.ts
839
1035
  function computeCompatibility(fw) {
840
1036
  const warnings = [];
@@ -1014,6 +1210,8 @@ async function runDetectors(root) {
1014
1210
  const package_manager = detectPackageManager(root);
1015
1211
  const credentials = detectCredentials(root, fw.env_prefix);
1016
1212
  const existing_integration = detectExistingIntegration(root);
1213
+ const version_conflict = detectVersionConflict(root, fw.framework);
1214
+ const coexistence = detectCoexistence(root, fw.framework);
1017
1215
  const compatibility = computeCompatibility(fw);
1018
1216
  const architecture_context = detectArchitectureContext(root);
1019
1217
  const project_name = detectProjectName(root);
@@ -1038,6 +1236,8 @@ async function runDetectors(root) {
1038
1236
  package_manager,
1039
1237
  credentials,
1040
1238
  existing_integration,
1239
+ version_conflict,
1240
+ coexistence,
1041
1241
  compatibility,
1042
1242
  architecture_context
1043
1243
  };
@@ -1153,6 +1353,32 @@ function printHumanReadable(r) {
1153
1353
  lines.push(` - ${f}`);
1154
1354
  }
1155
1355
  }
1356
+ if (r.version_conflict) {
1357
+ lines.push("");
1358
+ lines.push(` \u26A0 VERSION CONFLICT (${r.version_conflict.kind}): ${r.version_conflict.installed}`);
1359
+ lines.push(` ${r.version_conflict.detail}`);
1360
+ lines.push(" \u2192 Reconcile this BEFORE integrating; the dispatcher will stop and ask.");
1361
+ }
1362
+ const co = r.coexistence;
1363
+ if (co && (co.competing_sdks.length > 0 || co.existing_firebase || co.existing_service_workers.length > 0)) {
1364
+ lines.push("");
1365
+ lines.push(" Coexistence (competing stack):");
1366
+ if (co.competing_sdks.length > 0) {
1367
+ lines.push(" \u26A0 Competing chat/calling/push SDK(s) detected:");
1368
+ for (const s of co.competing_sdks) {
1369
+ lines.push(` - ${s.name} (${s.kind}) \u2014 ${s.coordinate}`);
1370
+ }
1371
+ lines.push(" \u2192 Dispatcher will ask: replace it, run alongside (namespaced), or switch fully?");
1372
+ }
1373
+ if (co.existing_firebase) {
1374
+ lines.push(" \u26A0 Existing Firebase setup \u2014 CometChat push is FCM-based; reuse the same app,");
1375
+ lines.push(" don't double-init, and merge (don't overwrite) firebase-messaging-sw.js.");
1376
+ }
1377
+ if (co.existing_service_workers.length > 0) {
1378
+ lines.push(" \u26A0 Existing service worker(s) \u2014 web push must not clobber:");
1379
+ for (const sw of co.existing_service_workers) lines.push(` - ${sw}`);
1380
+ }
1381
+ }
1156
1382
  lines.push("");
1157
1383
  lines.push(` Compatibility: ${r.compatibility.supported ? "supported" : "NOT supported"}`);
1158
1384
  if (r.compatibility.warnings.length > 0) {
@@ -3455,10 +3681,10 @@ function checkChatInitBeforeCallsInit(root, family) {
3455
3681
  for (const f of files) {
3456
3682
  const content = readFileOrNull(p(root, f));
3457
3683
  if (!content) continue;
3458
- const chatInit = /CometChat(UIKit)?\.init|CometChat\.init\(/.exec(content);
3459
- const callsInit = /CometChatCalls\.init|CometChatUIKitCalls\.init/.exec(content);
3684
+ const code = stripComments(content);
3685
+ const chatInit = /CometChat(UIKit)?\.init|CometChat\.init\(/.exec(code);
3686
+ const callsInit = /CometChatCalls\.init|CometChatUIKitCalls\.init/.exec(code);
3460
3687
  if (!chatInit || !callsInit) {
3461
- if (chatInit && !callsInit) return { status: "fail", reason: `${f}: chat init found but no Calls SDK init in same file` };
3462
3688
  continue;
3463
3689
  }
3464
3690
  if (chatInit.index < callsInit.index) return { status: "pass" };
@@ -3467,6 +3693,9 @@ function checkChatInitBeforeCallsInit(root, family) {
3467
3693
  return { status: "skip", reason: "no init file containing both Chat + Calls SDK init found" };
3468
3694
  }
3469
3695
  function checkIncomingCallRootMount(root, family) {
3696
+ if ((family === "web" || family === "native" || family === "angular") && isSessionOnlyMode(root, family)) {
3697
+ return { status: "skip", reason: "session/meeting mode \u2014 no incoming-call entity (joinSession; no ringing surface)" };
3698
+ }
3470
3699
  if (pathExists(p(root, ".cometchat/builder.json"))) {
3471
3700
  const homeCandidates = [
3472
3701
  "src/CometChat/components/CometChatHome/CometChatHome.tsx",
@@ -3613,14 +3842,20 @@ function checkHangupTeardown(root, family) {
3613
3842
  }
3614
3843
  }
3615
3844
  const requiredByFamily = {
3616
- web: [/CometChatCalls\.(leaveSession|endSession)/, /\.getTracks\(\)\.forEach\([^)]*\.stop\(\)\)/],
3845
+ web: [/CometChatCalls\.(leaveSession|endSession)/, /\.getTracks\(\)\.forEach\([\s\S]*?\.stop\(\s*\)\s*\)/],
3617
3846
  native: [/CometChatCalls\.(leaveSession|endSession)/, /RNCallKeep\.endCall/],
3618
- angular: [/CometChatCalls\.(leaveSession|endSession)/, /\.getTracks\(\)\.forEach\([^)]*\.stop\(\)\)/],
3847
+ angular: [/CometChatCalls\.(leaveSession|endSession)/, /\.getTracks\(\)\.forEach\([\s\S]*?\.stop\(\s*\)\s*\)/],
3619
3848
  android: [/CallSession\.getInstance\(\)\.leaveSession|CometChatCalls\.endSession|CometChatUIKit\.endSession|leaveSession\(\)/, /stopService|stopForeground|finish\(\)/],
3620
3849
  ios: [/CallSession\.shared\.leaveSession|CometChatCalls\.(leaveSession|endSession)/, /setActive\(false[^)]*notifyOthersOnDeactivation/],
3621
3850
  flutter: [/CallSession\.getInstance\(\)\.leaveSession|CometChatUIKitCalls\.endSession|CometChatCalls\.(leaveSession|endSession)/, /FlutterCallkitIncoming\.endAllCalls|Navigator\.of\(.*\)\.pop|CometChatOngoingCallService\.abort/]
3622
3851
  };
3623
- const required = requiredByFamily[family];
3852
+ let required = requiredByFamily[family];
3853
+ if (family === "web" || family === "angular") {
3854
+ const ownsMedia = anySourceFileMatches(root, family, /getUserMedia\s*\(/);
3855
+ if (!ownsMedia) {
3856
+ required = [/CometChatCalls\.(leaveSession|endSession)/];
3857
+ }
3858
+ }
3624
3859
  const exts = family === "ios" ? [".swift"] : family === "android" ? [".kt", ".java"] : family === "flutter" ? [".dart"] : [".ts", ".tsx", ".js", ".jsx"];
3625
3860
  const dirs = family === "ios" ? ["ios"] : family === "android" ? ["app/src/main"] : family === "flutter" ? ["lib"] : ["src", "app"];
3626
3861
  let foundAll = true;
@@ -3762,6 +3997,28 @@ function gatherSourceFiles(root, family) {
3762
3997
  const re = SOURCE_EXTS_FOR[family];
3763
3998
  return walkFiles(startDir, (name) => re.test(name));
3764
3999
  }
4000
+ function anySourceFileMatches(root, family, re) {
4001
+ for (const file of gatherSourceFiles(root, family)) {
4002
+ const content = readFileOrNull(file) ?? "";
4003
+ if (re.test(content)) return true;
4004
+ }
4005
+ return false;
4006
+ }
4007
+ function isSessionOnlyMode(root, family) {
4008
+ const usesSession = anySourceFileMatches(root, family, /CometChatCalls\.(joinSession|startSession)/);
4009
+ if (!usesSession) return false;
4010
+ const usesRinging = anySourceFileMatches(
4011
+ root,
4012
+ family,
4013
+ /CometChatIncomingCall|CometChatOutgoingCall|cometchat-incoming-call|cometchat-outgoing-call|\.initiateCall\b/
4014
+ );
4015
+ return !usesRinging;
4016
+ }
4017
+ function hasChatSdkInPackage(root, family) {
4018
+ const pkg = readFileOrNull(p(root, "package.json")) ?? "";
4019
+ const chatSdkPkg = family === "native" ? "@cometchat/chat-sdk-react-native" : "@cometchat/chat-sdk-javascript";
4020
+ return new RegExp(`"${chatSdkPkg.replace(/[/-]/g, "\\$&")}"`).test(pkg);
4021
+ }
3765
4022
  function checkEventListenerCleanup(root, family) {
3766
4023
  if (family === "ios" || family === "android" || family === "flutter") {
3767
4024
  return { status: "skip", reason: "native families use interface-based listeners, not addEventListener" };
@@ -3771,17 +4028,20 @@ function checkEventListenerCleanup(root, family) {
3771
4028
  for (const file of files) {
3772
4029
  const content = readFileOrNull(file) ?? "";
3773
4030
  if (!/CometChatCalls\.addEventListener/.test(content)) continue;
4031
+ const hasLegacyRemove = /CometChatCalls\.removeEventListener\s*\(/.test(content);
4032
+ const hasAbortSignal = /addEventListener\s*\([\s\S]*?\bsignal\b/.test(content) || /AbortController/.test(content);
4033
+ const capturesReturn = /(?:const|let|var)\s+\w+\s*(?::[^=\n]+)?=\s*CometChatCalls\.addEventListener/.test(content) || /(?:const|let|var)\s+\w+\s*(?::[^=\n]+)?=\s*\[[\s\S]*?CometChatCalls\.addEventListener/.test(content) || /return\s+CometChatCalls\.addEventListener/.test(content);
4034
+ const invokesUnsub = /\b\w*off\w*\s*\(\s*\)/i.test(content) || /=>\s*\w+\s*\(\s*\)/.test(content) || /\.forEach\(\s*\(?\s*\w+\s*\)?\s*=>/.test(content);
4035
+ if (hasLegacyRemove || hasAbortSignal || capturesReturn && invokesUnsub) continue;
3774
4036
  const adds = [...content.matchAll(/CometChatCalls\.addEventListener\(\s*["']([\w-]+)["']/g)].map((m) => m[1]);
3775
- const removes = [...content.matchAll(/CometChatCalls\.removeEventListener\(\s*["']([\w-]+)["']/g)].map((m) => m[1]);
3776
- const missing = adds.filter((evt) => !removes.includes(evt));
3777
- if (missing.length > 0) {
3778
- offenders.push(`${file.replace(root + "/", "")}: missing removeEventListener for [${[...new Set(missing)].join(", ")}]`);
3779
- }
4037
+ offenders.push(
4038
+ `${file.replace(root + "/", "")}: addEventListener (${[...new Set(adds)].join(", ")}) without invoking the returned unsubscribe \u2014 v5 returns an off() fn; call it in teardown`
4039
+ );
3780
4040
  }
3781
4041
  if (offenders.length === 0) return { status: "pass" };
3782
4042
  return {
3783
4043
  status: "fail",
3784
- reason: `${offenders.length} file(s) call addEventListener without a matching removeEventListener \u2014 listener leak`,
4044
+ reason: `${offenders.length} file(s) call addEventListener without invoking the returned unsubscribe \u2014 listener leak`,
3785
4045
  details: offenders.slice(0, 5).join("\n")
3786
4046
  };
3787
4047
  }
@@ -3946,7 +4206,7 @@ function checkVirtualBgPlatformGate(root, family) {
3946
4206
  if (family === "web" || family === "angular") return { status: "skip", reason: "platform supports virtual background" };
3947
4207
  const files = gatherSourceFiles(root, family);
3948
4208
  for (const file of files) {
3949
- const content = readFileOrNull(file) ?? "";
4209
+ const content = stripComments(readFileOrNull(file) ?? "");
3950
4210
  if (/hideVirtualBackgroundButton\s*[:=]\s*false/i.test(content) || /setHideVirtualBackgroundButton\(\s*false\s*\)/i.test(content)) {
3951
4211
  return {
3952
4212
  status: "fail",
@@ -3960,12 +4220,15 @@ function checkGroupAsSessionHelper(root, family) {
3960
4220
  if (family === "ios" || family === "android" || family === "flutter") {
3961
4221
  return { status: "skip", reason: "currently only checked for JS families" };
3962
4222
  }
4223
+ if (!hasChatSdkInPackage(root, family)) {
4224
+ return { status: "skip", reason: "no Chat SDK in project \u2014 calls-only session mode; in-call chat uses the Calls SDK's built-in chat, no CometChat group needed" };
4225
+ }
3963
4226
  const files = gatherSourceFiles(root, family);
3964
4227
  let usesInCallChat = false;
3965
4228
  let hasGroupHelper = false;
3966
4229
  for (const file of files) {
3967
4230
  const content = readFileOrNull(file) ?? "";
3968
- if (/onChatButtonClicked|hideChatButton\s*[:=]\s*false/.test(content)) usesInCallChat = true;
4231
+ if (/hideChatButton\s*[:=]\s*false/.test(content)) usesInCallChat = true;
3969
4232
  if (/createGroup|ensureCallGroup|ensureGroup|getGroup\s*\(/.test(content)) hasGroupHelper = true;
3970
4233
  }
3971
4234
  if (!usesInCallChat) return { status: "skip", reason: "in-call chat not wired" };
@@ -3977,7 +4240,7 @@ function checkCallTokenServerSide(root, family) {
3977
4240
  }
3978
4241
  const files = gatherSourceFiles(root, family);
3979
4242
  for (const file of files) {
3980
- const content = readFileOrNull(file) ?? "";
4243
+ const content = stripComments(readFileOrNull(file) ?? "");
3981
4244
  if (/COMETCHAT_AUTH_KEY|cometchat[_.]?auth[_.]?key/i.test(content) && /generateToken|callToken|getCallToken/i.test(content)) {
3982
4245
  return {
3983
4246
  status: "fail",
@@ -4086,6 +4349,9 @@ function checkRecordingConsentUi(root, family) {
4086
4349
  reason: "recording is enabled but no consent/notice UI found in source \u2014 required for two-party-consent jurisdictions"
4087
4350
  };
4088
4351
  }
4352
+ function stripComments(src) {
4353
+ return src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/[^\n]*/g, "$1");
4354
+ }
4089
4355
  function checkNoStaleSdkApis(root, family) {
4090
4356
  const exts = family === "ios" ? [".swift"] : family === "android" ? [".kt", ".java"] : family === "flutter" ? [".dart"] : [".ts", ".tsx", ".js", ".jsx"];
4091
4357
  const dirs = family === "ios" ? ["ios"] : family === "android" ? ["app/src/main"] : family === "flutter" ? ["lib"] : ["src", "app"];
@@ -4135,7 +4401,7 @@ function checkNoStaleSdkApis(root, family) {
4135
4401
  if (isDirectory(ep)) {
4136
4402
  if (!entry.startsWith(".") && entry !== "node_modules" && entry !== "Pods" && entry !== "build" && entry !== ".dart_tool") stack.push(ep);
4137
4403
  } else if (exts.some((e) => entry.endsWith(e))) {
4138
- const content = readFileOrNull(ep) ?? "";
4404
+ const content = stripComments(readFileOrNull(ep) ?? "");
4139
4405
  for (const { pattern, reason } of banned) {
4140
4406
  if (pattern.test(content)) {
4141
4407
  violations.push(`${ep.replace(root + "/", "")}: ${reason}`);
@@ -7009,6 +7275,10 @@ async function getCurrentUserWithLastApp(host, token) {
7009
7275
  }
7010
7276
  return { ...user, last_app };
7011
7277
  }
7278
+ function isExtensionEnabled(e) {
7279
+ if (!e) return false;
7280
+ return e.enabled === 1 || e.enabled === true || e.isActive === true || e.status === "enabled";
7281
+ }
7012
7282
  async function listInstalledExtensions(host, token, appId) {
7013
7283
  const res = await request(
7014
7284
  host,
@@ -7040,7 +7310,7 @@ async function toggleExtension(host, token, appId, extensionId, action) {
7040
7310
  }
7041
7311
  }
7042
7312
  async function toggleAiFeature(host, token, appId, featureKey, action) {
7043
- const path = `/apps/${encodeURIComponent(appId)}/features/ai.${encodeURIComponent(featureKey)}/enabled`;
7313
+ const path = `/apps/${encodeURIComponent(appId)}/features/${encodeURIComponent(featureKey)}/enabled`;
7044
7314
  const res = await request(host, path, {
7045
7315
  method: action === "enable" ? "POST" : "DELETE",
7046
7316
  headers: { Authorization: `Bearer ${token}` },
@@ -7516,6 +7786,8 @@ async function featuresToggle(args, featureName, action) {
7516
7786
  }
7517
7787
  const verb = action === "enable" ? "Enabled" : "Disabled";
7518
7788
  const autoWiredNote = match.auto_wired_in_uikit === true ? " The UI Kit's initiateAfterLogin() auto-attaches the decorator, so a browser refresh is all you need." : " Register the extension via UIKitSettingsBuilder.setExtensions([...]) if you haven't already \u2014 the toggle alone isn't enough for non-default extensions. Query the docs MCP for the exact builder syntax.";
7789
+ const codeNote = match.code === "custom-code" ? `This feature needs CUSTOM CLIENT CODE (tier 5). The reference implementation is in the docs (${match.docs_topic ?? "ui-kit/react/extensions"}) \u2014 fetch it via the docs MCP and adapt it; do NOT hand-roll.` : match.code === "steps-in-docs" ? `This feature needs client wiring \u2014 follow the documented steps at ${match.docs_topic ?? "ui-kit/react/extensions"} (query the docs MCP).` : match.code === "stitch-components" ? "This feature is surfaced by COMPOSING existing UI Kit components \u2014 see the kit sample app for the wiring pattern." : "";
7790
+ const settingsNote = match.dashboard_settings ? `Additional dashboard setup required: ${match.dashboard_settings}.` : "";
7519
7791
  const result = {
7520
7792
  status: action === "enable" ? "enabled" : "disabled",
7521
7793
  feature: match,
@@ -7523,8 +7795,10 @@ async function featuresToggle(args, featureName, action) {
7523
7795
  next_steps: [
7524
7796
  `${verb} ${match.name} on app ${appId}.`,
7525
7797
  "Hard-refresh (Cmd+Shift+R / Ctrl+Shift+R) the browser tab running your dev server to pick up the change.",
7526
- autoWiredNote.trim()
7527
- ]
7798
+ autoWiredNote.trim(),
7799
+ settingsNote,
7800
+ codeNote
7801
+ ].filter(Boolean)
7528
7802
  };
7529
7803
  if (json) {
7530
7804
  console.log(JSON.stringify(result, null, 2));
@@ -8919,7 +9193,7 @@ async function applyFeature(args) {
8919
9193
  ],
8920
9194
  "dashboard-only": [
8921
9195
  `${feature.name} is configured manually in the dashboard. Disable it there:`,
8922
- feature.dashboard_path ? ` Dashboard \u2192 ${feature.dashboard_path} \u2192 Disable` : ` Dashboard \u2192 Extensions \u2192 ${feature.name} \u2192 Disable`
9196
+ feature.dashboard_path ? ` Dashboard \u2192 ${feature.dashboard_path} \u2192 Disable` : ` Dashboard \u2192 Chat & Messaging \u2192 Features \u2192 ${feature.name} \u2192 Disable`
8923
9197
  ],
8924
9198
  "package-install": [
8925
9199
  `${feature.name} required installing a package. To remove:`,
@@ -9075,6 +9349,7 @@ async function applyModeration(args, result, root, state2, explicitAppId, featur
9075
9349
  result.feature_type = "moderation";
9076
9350
  result.next_steps = [
9077
9351
  `${feature.name} configured and enabled on app ${appId}. Config keys: ${Object.keys(config2).join(", ")}.`,
9352
+ "\u26A0\uFE0F LEGACY moderation extension. The canonical moderation system is now Rules Management (Dashboard \u2192 Moderation \u2192 Settings \u2192 Rules, or the /moderation/rules REST API) \u2014 rules auto-apply to all messages with no client code. If you use moderation Rules, DISABLE legacy extensions like this one first: running BOTH double-processes every message (delays + perf issues). See docs/moderation/overview.",
9078
9353
  ...feature.docs_topic ? [`Docs: https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}`] : []
9079
9354
  ];
9080
9355
  return outputResult11(args, result, 0);
@@ -9230,11 +9505,31 @@ async function applyExtensionToggle(args, result, root, state2, explicitAppId, f
9230
9505
  return outputResult11(args, result, 1);
9231
9506
  }
9232
9507
  const remove = args.flags.remove === true || args.flags.remove === "true";
9508
+ const extensionKey = feature.extension_id ?? feature.id;
9233
9509
  try {
9234
- await toggleExtension(host, creds.token, appId, feature.id, remove ? "disable" : "enable");
9510
+ await toggleExtension(host, creds.token, appId, extensionKey, remove ? "disable" : "enable");
9235
9511
  } catch (err) {
9236
9512
  return handleApiError(args, result, err, feature);
9237
9513
  }
9514
+ let lagWarning = null;
9515
+ try {
9516
+ const installed = await listInstalledExtensions(host, creds.token, appId);
9517
+ const match = installed.find((e) => e.id === extensionKey);
9518
+ if (!remove && !match) {
9519
+ result.status = "error";
9520
+ result.error = `The dashboard accepted the request but extension "${extensionKey}" did not appear in this app's extension list afterward \u2014 the extension key is wrong for this feature, or it isn't available on your plan.`;
9521
+ const enabledNow = installed.filter((e) => isExtensionEnabled(e)).map((e) => e.id).join(", ") || "(none)";
9522
+ result.next_steps = [
9523
+ `Extensions currently enabled on this app: ${enabledNow}.`,
9524
+ `Confirm "${feature.name}" is available on your plan (dashboard \u2192 Chat & Messaging \u2192 Features), then retry.`
9525
+ ];
9526
+ return outputResult11(args, result, 1);
9527
+ }
9528
+ if (!remove && match && !isExtensionEnabled(match)) {
9529
+ lagWarning = `Note: the toggle was accepted but the dashboard does not yet report "${feature.name}" as enabled. This is usually propagation lag \u2014 re-check in the dashboard (Chat & Messaging \u2192 Features) in a moment; re-run this command if it hasn't flipped.`;
9530
+ }
9531
+ } catch {
9532
+ }
9238
9533
  if (state2) {
9239
9534
  if (remove) {
9240
9535
  state2.applied_features = (state2.applied_features ?? []).filter((f) => f !== feature.id);
@@ -9247,7 +9542,7 @@ async function applyExtensionToggle(args, result, root, state2, explicitAppId, f
9247
9542
  summary: `${remove ? "Disabled" : "Enabled"} extension "${feature.name}" via dashboard API on app ${appId}`,
9248
9543
  inputs: { feature: feature.id, appId, remove },
9249
9544
  decisions: {
9250
- api_endpoint: `POST /apps/${appId}/extensions { ${remove ? "disabled" : "enabled"}: ["${feature.id}"] }`,
9545
+ api_endpoint: `POST /apps/${appId}/extensions { ${remove ? "disabled" : "enabled"}: ["${extensionKey}"] }`,
9251
9546
  auto_wired: feature.auto_wired_in_uikit ? "yes" : "no"
9252
9547
  },
9253
9548
  files_modified: [],
@@ -9265,6 +9560,7 @@ async function applyExtensionToggle(args, result, root, state2, explicitAppId, f
9265
9560
  feature.auto_wired_in_uikit ? "The UI Kit's defaultExtensions[] picks this up automatically \u2014 refresh the browser." : "Refresh the chat in the browser. If the feature requires UIKitSettingsBuilder.setExtensions([...]), see the docs link below.",
9266
9561
  ...feature.docs_topic ? [`Docs: https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}`] : []
9267
9562
  ];
9563
+ if (lagWarning) result.next_steps = [lagWarning, ...result.next_steps];
9268
9564
  return outputResult11(args, result, 0);
9269
9565
  }
9270
9566
  async function applyAiFeatureToggle(args, result, root, state2, explicitAppId, feature) {
@@ -9335,7 +9631,7 @@ async function applyAiFeatureToggle(args, result, root, state2, explicitAppId, f
9335
9631
  inputs: { feature: feature.id, appId, openai_key_written: !remove && needsKeyWrite, remove },
9336
9632
  decisions: {
9337
9633
  settings_endpoint: !remove && needsKeyWrite ? `PUT /apps/${appId}/ai/settings { openAIKey: "(redacted)" }` : "skipped",
9338
- toggle_endpoint: `${remove ? "DELETE" : "POST"} /apps/${appId}/features/ai.${feature.ai_key}/enabled`
9634
+ toggle_endpoint: `${remove ? "DELETE" : "POST"} /apps/${appId}/features/${feature.ai_key}/enabled`
9339
9635
  },
9340
9636
  files_modified: [],
9341
9637
  next_actions: [
@@ -9359,7 +9655,7 @@ function applyDashboardOnly(args, result, feature) {
9359
9655
  result.status = "manual-action-required";
9360
9656
  result.error = `${feature.name} requires config beyond a boolean toggle (third-party API key or multi-field setup).`;
9361
9657
  result.next_steps = [
9362
- feature.dashboard_path ? `In the dashboard at https://app.cometchat.com \u2192 ${feature.dashboard_path}` : `Open https://app.cometchat.com and configure ${feature.name} on the Extensions page.`,
9658
+ feature.dashboard_path ? `In the dashboard at https://app.cometchat.com \u2192 ${feature.dashboard_path}` : `Open https://app.cometchat.com and configure ${feature.name} on the Chat & Messaging \u2192 Features page.`,
9363
9659
  ...feature.docs_topic ? [`Docs: https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}`] : []
9364
9660
  ];
9365
9661
  return outputResult11(args, result, 1);
@@ -11794,6 +12090,32 @@ import { spawnSync as spawnSync5 } from "node:child_process";
11794
12090
  import { tmpdir } from "node:os";
11795
12091
  init_config();
11796
12092
  init_fs();
12093
+ var BUILDER_TOGGLE_TO_DASHBOARD_FEATURE = {
12094
+ deeperUserEngagement: {
12095
+ messageTranslation: { id: "message-translation", type: "extension" },
12096
+ polls: { id: "polls", type: "extension" },
12097
+ collaborativeWhiteboard: { id: "collaborative-whiteboard", type: "extension" },
12098
+ collaborativeDocument: { id: "collaborative-document", type: "extension" },
12099
+ stickers: { id: "stickers", type: "extension" }
12100
+ },
12101
+ aiUserCopilot: {
12102
+ conversationStarter: { id: "conversation-starter", type: "ai-feature" },
12103
+ conversationSummary: { id: "conversation-summary", type: "ai-feature" },
12104
+ smartReply: { id: "smart-replies", type: "ai-feature" }
12105
+ }
12106
+ };
12107
+ function dashboardSetupNeeded(chatFeatures) {
12108
+ const out = [];
12109
+ if (!chatFeatures) return out;
12110
+ for (const [section, keyMap] of Object.entries(BUILDER_TOGGLE_TO_DASHBOARD_FEATURE)) {
12111
+ const block = chatFeatures[section];
12112
+ if (!block || typeof block !== "object") continue;
12113
+ for (const [builderKey, feat] of Object.entries(keyMap)) {
12114
+ if (block[builderKey] === true) out.push({ ...feat, builderKey, section });
12115
+ }
12116
+ }
12117
+ return out;
12118
+ }
11797
12119
  var HELP20 = `
11798
12120
  cometchat builder \u2014 manage Visual Chat Builders for an app
11799
12121
 
@@ -12120,6 +12442,11 @@ var PLATFORM_LAYOUTS = {
12120
12442
  // the JSON settings. SKILL.md §"Visual Builder integration" lists
12121
12443
  // these as verbatim copies. ThreadedMessagesVC.swift is included
12122
12444
  // because MessagesVC.swift imports it.
12445
+ // BUILD-BREAKER (ENG-35337): the verbatim ThreadedMessagesVC.swift reads
12446
+ // CometChatBuilderSettings.shared.layout.compactMessageComposer, which is
12447
+ // MISSING in CometChatBuilder <= 1.1.2 -> compile failure. Pin
12448
+ // `pod 'CometChatBuilder', '> 1.1.2'` (SPM: from: "1.1.3"). The reference
12449
+ // app's Podfile is unversioned -- that's the trap, not the fix.
12123
12450
  files: [
12124
12451
  {
12125
12452
  zipPath: "CometChatBuilderSwift/BuilderApp/View Controllers/CometChat Components/MessagesVC.swift",
@@ -12286,7 +12613,9 @@ async function builderExport(args) {
12286
12613
  }
12287
12614
  const layout = PLATFORM_LAYOUTS[platform];
12288
12615
  if (!layout) {
12289
- const msg = `\`builder export --platform ${platform}\` is not yet implemented. v4.3.0 ships React only. For ${platform}, follow the manual recipe in cometchat-{${platform === "react-native" ? "native" : platform === "ios" ? "ios" : platform}}-core/SKILL.md \xA7"Visual Builder integration".`;
12616
+ const supported = Object.keys(PLATFORM_LAYOUTS).join(", ");
12617
+ const coreSkill = platform === "react-native" ? "native" : platform;
12618
+ const msg = `\`builder export --platform ${platform}\` is not yet implemented (supported: ${supported}). For ${platform}, follow the manual recipe in cometchat-${coreSkill}-core/SKILL.md \xA7"Visual Builder integration".`;
12290
12619
  if (json) return emitError(true, msg);
12291
12620
  console.error(msg);
12292
12621
  return 1;
@@ -12360,6 +12689,7 @@ async function builderExport(args) {
12360
12689
  const due = chatFeatures.deeperUserEngagement;
12361
12690
  if (due && due.mentionAll === void 0) due.mentionAll = true;
12362
12691
  }
12692
+ const needSetup = dashboardSetupNeeded(chatFeatures);
12363
12693
  const tmpRoot = mkdtempSync(join18(tmpdir(), "cometchat-builder-export-"));
12364
12694
  const tmpZip = join18(tmpRoot, `cometchat-builder-${platform}.zip`);
12365
12695
  const tmpExtract = join18(tmpRoot, "extracted");
@@ -12381,9 +12711,7 @@ async function builderExport(args) {
12381
12711
  }
12382
12712
  const settingsContent = (() => {
12383
12713
  if (layout.format === "ts-export") {
12384
- const body2 = `export const CometChatSettings = ${JSON.stringify(settings, null, 2)};
12385
- `;
12386
- return layout.sentinel ? injectSentinel(body2, builderId) : body2;
12714
+ return `export const CometChatSettings: CometChatSettingsInterface = ${JSON.stringify(settings, null, 2)};`;
12387
12715
  }
12388
12716
  const nowSec = Math.floor(Date.now() / 1e3);
12389
12717
  const envelope = {
@@ -12419,7 +12747,15 @@ async function builderExport(args) {
12419
12747
  console.error(msg);
12420
12748
  return 1;
12421
12749
  }
12422
- writeFileSync10(settingsPath, settingsContent);
12750
+ if (layout.format === "ts-export") {
12751
+ const existing = readFileSync16(settingsPath, "utf8");
12752
+ const constRe = /export const CometChatSettings(?::\s*[A-Za-z_][\w]*)?\s*=\s*\{[\s\S]*?\n\};/;
12753
+ let patched = constRe.test(existing) ? existing.replace(constRe, settingsContent) : existing.replace(/\n*$/, "\n\n") + settingsContent + "\n";
12754
+ if (layout.sentinel) patched = injectSentinel(patched, builderId);
12755
+ writeFileSync10(settingsPath, patched);
12756
+ } else {
12757
+ writeFileSync10(settingsPath, settingsContent);
12758
+ }
12423
12759
  primarySettingsFile = settingsPath;
12424
12760
  } else {
12425
12761
  if (existsSync4(outputAbs) && force) rmSync3(outputAbs, { recursive: true, force: true });
@@ -12470,7 +12806,12 @@ async function builderExport(args) {
12470
12806
  platform,
12471
12807
  output: outputAbs,
12472
12808
  settings_file: primarySettingsFile,
12473
- builder_name: builderName
12809
+ builder_name: builderName,
12810
+ // Toggled-ON builder features that ALSO need dashboard enablement
12811
+ // (extension / AI key). Empty array = nothing extra to enable. The
12812
+ // dispatcher runs `cometchat apply-feature <id>` for each `extension`
12813
+ // and prompts for an OpenAI key for each `ai-feature`.
12814
+ dashboardSetupNeeded: needSetup
12474
12815
  },
12475
12816
  null,
12476
12817
  2
@@ -12480,6 +12821,15 @@ async function builderExport(args) {
12480
12821
  console.log(`\u2713 Builder exported: ${builderId} \u2192 ${outputAbs}`);
12481
12822
  console.log(` Settings: ${primarySettingsFile}${layout.sentinel ? " (patched + sentinel)" : " (patched)"}`);
12482
12823
  console.log(` Platform: ${platform}`);
12824
+ if (needSetup.length) {
12825
+ console.log("");
12826
+ console.log(" \u26A0 These features are ON in your builder but also need DASHBOARD setup");
12827
+ console.log(" (the builder shows their UI; the server-side capability is separate):");
12828
+ for (const f of needSetup) {
12829
+ const how = f.type === "ai-feature" ? `cometchat apply-feature ${f.id} --openai-key sk-...` : `cometchat apply-feature ${f.id}`;
12830
+ console.log(` \u2022 ${f.id} \u2192 ${how}`);
12831
+ }
12832
+ }
12483
12833
  console.log("");
12484
12834
  console.log("Resync later by re-running this exact command \u2014 `--force` to replace.");
12485
12835
  }