@cometchat/skills-cli 2.1.0 → 2.3.0

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
@@ -283,6 +283,9 @@ var init_state = __esm({
283
283
  // src/commands/detect.ts
284
284
  import { resolve } from "node:path";
285
285
 
286
+ // src/detectors/framework.ts
287
+ import { readdirSync } from "node:fs";
288
+
286
289
  // src/utils/version.ts
287
290
  function stripRange(versionRange) {
288
291
  return versionRange.replace(/^[\^~>=<\s]+/, "").trim();
@@ -309,7 +312,12 @@ var EMPTY = {
309
312
  router: null,
310
313
  bundler: null,
311
314
  ssr_strategy: null,
312
- env_prefix: null
315
+ env_prefix: null,
316
+ expo_mode: null,
317
+ expo_version: null,
318
+ react_native_version: null,
319
+ android_version: null,
320
+ flutter_version: null
313
321
  };
314
322
  function allDeps(pkg) {
315
323
  return {
@@ -320,6 +328,50 @@ function allDeps(pkg) {
320
328
  function hasViteConfig(root) {
321
329
  return pathExists(p(root, "vite.config.ts")) || pathExists(p(root, "vite.config.js")) || pathExists(p(root, "vite.config.mjs")) || pathExists(p(root, "vite.config.mts"));
322
330
  }
331
+ function detectExpo(root, deps, pkg) {
332
+ if (!deps["expo"]) return null;
333
+ const expoVersion = extractVersion(deps["expo"]);
334
+ const rnVersion = deps["react-native"] ? extractVersion(deps["react-native"]) : null;
335
+ const hasExpoRouter = !!deps["expo-router"];
336
+ const mainIsExpoRouter = pkg.main === "expo-router/entry" || pkg.main === "expo-router/entry.js";
337
+ const hasAppLayout = pathExists(p(root, "app/_layout.tsx")) || pathExists(p(root, "app/_layout.ts")) || pathExists(p(root, "app/_layout.jsx")) || pathExists(p(root, "app/_layout.js"));
338
+ const usingExpoRouter = hasExpoRouter && (mainIsExpoRouter || hasAppLayout);
339
+ const expoMode = usingExpoRouter ? "expo-router" : "managed";
340
+ return {
341
+ framework: "expo",
342
+ framework_version: expoVersion,
343
+ router: usingExpoRouter ? "expo-router" : "react-navigation",
344
+ bundler: "metro",
345
+ ssr_strategy: "native-no-ssr",
346
+ env_prefix: "EXPO_PUBLIC_",
347
+ expo_mode: expoMode,
348
+ expo_version: expoVersion,
349
+ react_native_version: rnVersion
350
+ };
351
+ }
352
+ function detectBareReactNative(root, deps) {
353
+ if (!deps["react-native"]) return null;
354
+ if (deps["expo"]) return null;
355
+ const hasNativeIos = pathExists(p(root, "ios")) && (pathExists(p(root, "ios/Podfile")) || pathExists(p(root, "ios/Podfile.properties.json")));
356
+ const hasNativeAndroid = pathExists(p(root, "android")) && pathExists(p(root, "android/build.gradle"));
357
+ const hasRnConfig = pathExists(p(root, "react-native.config.js")) || pathExists(p(root, "react-native.config.ts"));
358
+ if (!hasNativeIos && !hasNativeAndroid && !hasRnConfig) {
359
+ return null;
360
+ }
361
+ const rnVersion = extractVersion(deps["react-native"]);
362
+ return {
363
+ framework: "react-native",
364
+ framework_version: rnVersion,
365
+ router: "react-navigation",
366
+ bundler: "metro",
367
+ ssr_strategy: "native-no-ssr",
368
+ env_prefix: null,
369
+ // Bare RN uses react-native-dotenv or a config module — no prefix
370
+ expo_mode: null,
371
+ expo_version: null,
372
+ react_native_version: rnVersion
373
+ };
374
+ }
323
375
  function detectNextjs(root, deps) {
324
376
  if (!deps["next"]) return null;
325
377
  const version = extractVersion(deps["next"]);
@@ -387,6 +439,144 @@ function detectAstro(_root, deps) {
387
439
  env_prefix: "PUBLIC_"
388
440
  };
389
441
  }
442
+ function detectIOS(root) {
443
+ let dirEntries = [];
444
+ try {
445
+ dirEntries = readdirSync(root);
446
+ } catch {
447
+ }
448
+ const hasXcode = dirEntries.some(
449
+ (n) => n.endsWith(".xcodeproj") || n.endsWith(".xcworkspace")
450
+ );
451
+ const podfile = readFileOrNull(p(root, "Podfile"));
452
+ const hasIOSPodfile = !!podfile && /platform\s+:ios/.test(podfile);
453
+ const packageSwift = readFileOrNull(p(root, "Package.swift"));
454
+ const hasIOSPackage = !!packageSwift && /\.iOS\s*\(/.test(packageSwift);
455
+ if (!hasXcode && !hasIOSPodfile && !hasIOSPackage) return null;
456
+ const podfileLock = readFileOrNull(p(root, "Podfile.lock")) ?? "";
457
+ const packageResolved = readFileOrNull(p(root, "Package.resolved")) ?? readFileOrNull(p(root, ".swiftpm/Package.resolved")) ?? "";
458
+ const cocoapodSources = (podfile ?? "") + podfileLock;
459
+ const spmSources = (packageSwift ?? "") + packageResolved;
460
+ let ios_version = null;
461
+ if (
462
+ // Podfile.lock: `- CometChatUIKitSwift (5.1.12)` / Podfile: `pod 'CometChatUIKitSwift', '~> 5.0'`
463
+ /CometChatUIKitSwift[^,\n]*?[\s'"(]5\./.test(cocoapodSources) || // Package.resolved: `"identity" : "cometchatuikitswift", ... "version" : "5.x.x"`
464
+ /cometchatuikitswift[\s\S]{0,300}"version"\s*:\s*"5\./i.test(spmSources) || // Package.swift: `.package(url: "https://github.com/cometchat/...CometChatUIKitSwift", from: "5.0.0")`
465
+ /CometChatUIKitSwift[\s\S]{0,200}from:\s*"5\./.test(spmSources)
466
+ ) {
467
+ ios_version = "v5";
468
+ }
469
+ return {
470
+ framework: "ios",
471
+ framework_version: null,
472
+ // Swift / iOS deployment target isn't load-bearing for the dispatcher
473
+ router: null,
474
+ bundler: null,
475
+ ssr_strategy: null,
476
+ env_prefix: "",
477
+ // No public-env-prefix; Secrets.swift / xcconfig is the convention
478
+ ios_version
479
+ };
480
+ }
481
+ function detectFlutter(root) {
482
+ const pubspecRaw = readFileOrNull(p(root, "pubspec.yaml"));
483
+ if (!pubspecRaw) return null;
484
+ const isFlutter = /\n\s*flutter:\s*\n\s*sdk:\s*flutter/.test(pubspecRaw) || /\n\s*flutter:\s*$/m.test(pubspecRaw) || /\n\s*flutter:\s*\n\s*uses-material-design/.test(pubspecRaw);
485
+ if (!isFlutter) return null;
486
+ let flutter_version = null;
487
+ if (/cometchat_chat_uikit\s*:\s*[\^~]?6\./.test(pubspecRaw)) {
488
+ flutter_version = "v6";
489
+ } else if (/cometchat_chat_uikit\s*:\s*[\^~]?5\./.test(pubspecRaw) || /cometchat_calls_uikit\s*:\s*[\^~]?5\./.test(pubspecRaw)) {
490
+ flutter_version = "v5";
491
+ }
492
+ const sdkMatch = pubspecRaw.match(/\n\s*environment:\s*\n\s*sdk:\s*['"]?([^'"\n]+)/);
493
+ const framework_version = sdkMatch ? sdkMatch[1].trim() : null;
494
+ return {
495
+ framework: "flutter",
496
+ framework_version,
497
+ router: null,
498
+ // Flutter routing is in code (Navigator/GoRouter); not modeled here
499
+ bundler: null,
500
+ // Dart compiler — no JS bundler
501
+ ssr_strategy: null,
502
+ env_prefix: "",
503
+ // No public-env-prefix; --dart-define or .env-derived constants
504
+ flutter_version
505
+ };
506
+ }
507
+ function detectAndroid(root) {
508
+ const hasSettingsGradle = pathExists(p(root, "settings.gradle")) || pathExists(p(root, "settings.gradle.kts"));
509
+ const hasBuildGradle = pathExists(p(root, "build.gradle")) || pathExists(p(root, "build.gradle.kts"));
510
+ if (!hasSettingsGradle && !hasBuildGradle) return null;
511
+ const hasAppModule = pathExists(p(root, "app/build.gradle")) || pathExists(p(root, "app/build.gradle.kts"));
512
+ const rootGradle = readFileOrNull(p(root, "build.gradle")) ?? readFileOrNull(p(root, "build.gradle.kts")) ?? "";
513
+ const settingsGradle = readFileOrNull(p(root, "settings.gradle")) ?? readFileOrNull(p(root, "settings.gradle.kts")) ?? "";
514
+ const hasAndroidPlugin = /com\.android\.(application|library)/.test(
515
+ rootGradle + settingsGradle
516
+ );
517
+ if (!hasAppModule && !hasAndroidPlugin) return null;
518
+ const buildFiles = [
519
+ "build.gradle",
520
+ "build.gradle.kts",
521
+ "app/build.gradle",
522
+ "app/build.gradle.kts"
523
+ ];
524
+ let allBuildContent = "";
525
+ for (const f of buildFiles) {
526
+ const c = readFileOrNull(p(root, f));
527
+ if (c) allBuildContent += "\n" + c;
528
+ }
529
+ let android_version = null;
530
+ if (/com\.cometchat:chatuikit-(compose|kotlin)-android:6/.test(allBuildContent)) {
531
+ android_version = "v6";
532
+ } else if (/com\.cometchat:chat-uikit-android:5/.test(allBuildContent)) {
533
+ android_version = "v5";
534
+ }
535
+ let android_language = null;
536
+ const usesKtsGradle = pathExists(p(root, "build.gradle.kts")) || pathExists(p(root, "app/build.gradle.kts")) || pathExists(p(root, "settings.gradle.kts"));
537
+ const hasKotlinPlugin = /(?:apply\s+plugin\s*:\s*['"]kotlin-android['"]|id\s*\(?\s*['"]org\.jetbrains\.kotlin\.android['"]|id\s*\(?\s*['"]kotlin-android['"])/.test(
538
+ allBuildContent
539
+ );
540
+ if (usesKtsGradle || hasKotlinPlugin) {
541
+ android_language = "kotlin";
542
+ } else {
543
+ const javaDir = p(root, "app/src/main/java");
544
+ const kotlinDir = p(root, "app/src/main/kotlin");
545
+ if (pathExists(kotlinDir)) android_language = "kotlin";
546
+ else if (pathExists(javaDir)) android_language = "java";
547
+ }
548
+ return {
549
+ framework: "android",
550
+ framework_version: null,
551
+ // Gradle/AGP version isn't load-bearing for the dispatcher
552
+ router: null,
553
+ bundler: null,
554
+ // No JS bundler for native Android
555
+ ssr_strategy: null,
556
+ env_prefix: "",
557
+ // No public-env-prefix — local.properties + BuildConfig is the convention
558
+ android_version,
559
+ android_language
560
+ };
561
+ }
562
+ function detectAngular(root, deps) {
563
+ const hasAngularJson = pathExists(p(root, "angular.json"));
564
+ const hasAngularCore = !!deps["@angular/core"];
565
+ if (!hasAngularJson && !hasAngularCore) return null;
566
+ const version = extractVersion(deps["@angular/core"] ?? "");
567
+ return {
568
+ framework: "angular",
569
+ framework_version: version,
570
+ router: null,
571
+ // Angular Router is a separate concern; not modeled here yet
572
+ bundler: "webpack",
573
+ // Angular CLI ships with webpack; esbuild for v17+ but irrelevant to integration
574
+ ssr_strategy: "spa-no-ssr",
575
+ // Angular Universal exists but the integration path is SPA
576
+ env_prefix: ""
577
+ // No public-env-prefix — environment.ts is the convention
578
+ };
579
+ }
390
580
  function detectReactjs(root, deps) {
391
581
  if (!deps["react"]) return null;
392
582
  const version = extractVersion(deps["react"]);
@@ -418,10 +608,16 @@ function detectUsesJsx(root) {
418
608
  return false;
419
609
  }
420
610
  function detectFramework(root) {
611
+ const flutterInfo = detectFlutter(root);
612
+ if (flutterInfo) return flutterInfo;
613
+ const iosInfo = detectIOS(root);
614
+ if (iosInfo) return iosInfo;
615
+ const androidInfo = detectAndroid(root);
616
+ if (androidInfo) return androidInfo;
421
617
  const pkg = readJsonOrNull(p(root, "package.json"));
422
618
  if (!pkg) return EMPTY;
423
619
  const deps = allDeps(pkg);
424
- return detectNextjs(root, deps) ?? detectAstro(root, deps) ?? detectReactRouter(root, deps) ?? detectReactjs(root, deps) ?? EMPTY;
620
+ return detectExpo(root, deps, pkg) ?? detectBareReactNative(root, deps) ?? detectNextjs(root, deps) ?? detectAstro(root, deps) ?? detectReactRouter(root, deps) ?? detectAngular(root, deps) ?? detectReactjs(root, deps) ?? EMPTY;
425
621
  }
426
622
 
427
623
  // src/detectors/package-manager.ts
@@ -436,7 +632,7 @@ function detectPackageManager(root) {
436
632
 
437
633
  // src/detectors/credentials.ts
438
634
  init_fs();
439
- import { readdirSync, statSync } from "node:fs";
635
+ import { readdirSync as readdirSync2, statSync } from "node:fs";
440
636
  import { join as join2, extname } from "node:path";
441
637
  function envVarsForPrefix(prefix) {
442
638
  const p2 = prefix ?? "";
@@ -504,7 +700,7 @@ function searchForInit(dir, maxDepth) {
504
700
  if (maxDepth < 0) return false;
505
701
  let entries;
506
702
  try {
507
- entries = readdirSync(dir);
703
+ entries = readdirSync2(dir);
508
704
  } catch {
509
705
  return false;
510
706
  }
@@ -556,7 +752,7 @@ function detectCredentials(root, envPrefix) {
556
752
  }
557
753
 
558
754
  // src/detectors/integration.ts
559
- import { readdirSync as readdirSync2 } from "node:fs";
755
+ import { readdirSync as readdirSync3 } from "node:fs";
560
756
  init_fs();
561
757
  function getInstalledVersion(root) {
562
758
  const pkg = readJsonOrNull(p(root, "package.json"));
@@ -579,7 +775,7 @@ function findCometchatDirs(root) {
579
775
  const fullPath = p(root, dir);
580
776
  if (!pathExists(fullPath)) continue;
581
777
  try {
582
- const entries = readdirSync2(fullPath);
778
+ const entries = readdirSync3(fullPath);
583
779
  for (const entry of entries) {
584
780
  found.push(`${dir}/${entry}`);
585
781
  }
@@ -633,10 +829,49 @@ function computeCompatibility(fw) {
633
829
  return {
634
830
  supported: false,
635
831
  warnings: [
636
- "Could not detect a supported React framework. CometChat skills v2 supports: reactjs (Vite/CRA), nextjs, react-router (v6/v7), astro."
832
+ "Could not detect a supported React framework. CometChat skills supports: reactjs (Vite/CRA), nextjs, react-router (v6/v7), astro, expo (managed + Expo Router), and react-native (bare CLI)."
637
833
  ]
638
834
  };
639
835
  }
836
+ if (fw.framework === "expo") {
837
+ if (fw.expo_version) {
838
+ const expoMajor = parseInt(fw.expo_version.split(".")[0] ?? "0", 10);
839
+ if (expoMajor < 49) {
840
+ return {
841
+ supported: false,
842
+ warnings: [
843
+ `Expo SDK ${fw.expo_version} is older than the tested baseline (49+). CometChat React Native UI Kit v5 requires Expo SDK 49 or newer. Run \`npx expo install expo\` to upgrade.`
844
+ ]
845
+ };
846
+ }
847
+ }
848
+ if (fw.react_native_version) {
849
+ const rnMinor = parseFloat(fw.react_native_version.split(".").slice(0, 2).join("."));
850
+ if (!isNaN(rnMinor) && rnMinor < 0.72) {
851
+ warnings.push(
852
+ `React Native ${fw.react_native_version} is below the recommended baseline (0.72+). Some UI Kit v5 features may not work.`
853
+ );
854
+ }
855
+ }
856
+ }
857
+ if (fw.framework === "react-native") {
858
+ if (fw.react_native_version) {
859
+ const rnMinor = parseFloat(fw.react_native_version.split(".").slice(0, 2).join("."));
860
+ if (!isNaN(rnMinor) && rnMinor < 0.7) {
861
+ return {
862
+ supported: false,
863
+ warnings: [
864
+ `React Native ${fw.react_native_version} is older than the tested baseline (0.70+). CometChat React Native UI Kit v5 requires RN 0.70 or newer.`
865
+ ]
866
+ };
867
+ }
868
+ if (rnMinor < 0.72) {
869
+ warnings.push(
870
+ `React Native ${fw.react_native_version} is below the recommended baseline (0.72+). Upgrade is recommended.`
871
+ );
872
+ }
873
+ }
874
+ }
640
875
  if (fw.framework === "nextjs") {
641
876
  if (fw.router === null) {
642
877
  warnings.push(
@@ -732,6 +967,34 @@ function detectArchitectureContext(root) {
732
967
  };
733
968
  }
734
969
 
970
+ // src/detectors/project-name.ts
971
+ init_fs();
972
+ function detectProjectName(root) {
973
+ const pkg = readJsonOrNull(p(root, "package.json"));
974
+ if (pkg?.name) return cleanName(pkg.name);
975
+ const pubspec = readFileOrNull(p(root, "pubspec.yaml"));
976
+ if (pubspec) {
977
+ const m = pubspec.match(/^\s*name\s*:\s*['"]?([A-Za-z_][A-Za-z0-9_]*)['"]?\s*$/m);
978
+ if (m) return cleanName(m[1]);
979
+ }
980
+ const packageSwift = readFileOrNull(p(root, "Package.swift"));
981
+ if (packageSwift) {
982
+ const m = packageSwift.match(/Package\s*\(\s*name\s*:\s*"([^"]+)"/);
983
+ if (m) return cleanName(m[1]);
984
+ }
985
+ for (const file of ["settings.gradle.kts", "settings.gradle"]) {
986
+ const content = readFileOrNull(p(root, file));
987
+ if (!content) continue;
988
+ const m = content.match(/rootProject\.name\s*=\s*['"]([^'"\n]+)['"]/);
989
+ if (m) return cleanName(m[1]);
990
+ }
991
+ return null;
992
+ }
993
+ function cleanName(raw) {
994
+ const trimmed = raw.trim().slice(0, 64);
995
+ return trimmed.length > 0 ? trimmed : null;
996
+ }
997
+
735
998
  // src/detectors/index.ts
736
999
  async function runDetectors(root) {
737
1000
  const fw = detectFramework(root);
@@ -740,6 +1003,7 @@ async function runDetectors(root) {
740
1003
  const existing_integration = detectExistingIntegration(root);
741
1004
  const compatibility = computeCompatibility(fw);
742
1005
  const architecture_context = detectArchitectureContext(root);
1006
+ const project_name = detectProjectName(root);
743
1007
  return {
744
1008
  project_root: root,
745
1009
  framework: fw.framework,
@@ -750,6 +1014,14 @@ async function runDetectors(root) {
750
1014
  env_prefix: fw.env_prefix,
751
1015
  uses_jsx: fw.uses_jsx,
752
1016
  react_router_mode: fw.react_router_mode,
1017
+ expo_mode: fw.expo_mode,
1018
+ expo_version: fw.expo_version,
1019
+ react_native_version: fw.react_native_version,
1020
+ android_version: fw.android_version,
1021
+ android_language: fw.android_language,
1022
+ flutter_version: fw.flutter_version,
1023
+ ios_version: fw.ios_version,
1024
+ project_name,
753
1025
  package_manager,
754
1026
  credentials,
755
1027
  existing_integration,
@@ -826,10 +1098,23 @@ function printHumanReadable(r) {
826
1098
  lines.push("");
827
1099
  lines.push(` Project: ${r.project_root}`);
828
1100
  lines.push(` Framework: ${r.framework ?? "unknown"}${r.framework_version ? ` (${r.framework_version})` : ""}`);
1101
+ if (r.expo_mode && r.expo_mode !== null) lines.push(` Expo mode: ${r.expo_mode}`);
1102
+ if (r.react_native_version && r.framework === "expo") {
1103
+ lines.push(` React Native: ${r.react_native_version}`);
1104
+ }
1105
+ if (r.framework === "android") {
1106
+ lines.push(` Android UIKit: ${r.android_version ?? "(none yet \u2014 greenfield)"}`);
1107
+ }
1108
+ if (r.framework === "flutter") {
1109
+ lines.push(` Flutter UIKit: ${r.flutter_version ?? "(none yet \u2014 greenfield)"}`);
1110
+ }
829
1111
  if (r.router !== null) lines.push(` Router: ${r.router}`);
830
1112
  if (r.bundler !== null) lines.push(` Bundler: ${r.bundler}`);
831
1113
  if (r.ssr_strategy !== null) lines.push(` SSR strategy: ${r.ssr_strategy}`);
832
1114
  if (r.env_prefix !== null) lines.push(` Env var prefix: ${r.env_prefix}`);
1115
+ if (r.framework === "react-native" && r.env_prefix === null) {
1116
+ lines.push(` Env var prefix: (none \u2014 bare RN uses react-native-dotenv or a config module)`);
1117
+ }
833
1118
  if (r.package_manager !== null) lines.push(` Package manager: ${r.package_manager}`);
834
1119
  if (r.uses_jsx === true) {
835
1120
  lines.push(` Source language: JavaScript (.jsx) \u2014 \u26A0 apply will refuse, the v6 templates are TypeScript-only`);
@@ -2890,6 +3175,73 @@ function checkInitBeforeLogin(root, ownedAndPatchedFiles) {
2890
3175
  }
2891
3176
  return { status: "skip", reason: "no file calling CometChatUIKit.init found" };
2892
3177
  }
3178
+ function checkGestureHandlerLine1(root) {
3179
+ const entryCandidates = ["index.js", "index.ts", "App.tsx", "App.jsx", "app/_layout.tsx", "app/_layout.js"];
3180
+ for (const e of entryCandidates) {
3181
+ if (!pathExists(p(root, e))) continue;
3182
+ const content = readFileOrNull(p(root, e));
3183
+ if (!content) continue;
3184
+ const firstLine = content.split(/\r?\n/)[0]?.trim() ?? "";
3185
+ if (/^import\s+['"]react-native-gesture-handler['"]/.test(firstLine)) {
3186
+ return { status: "pass" };
3187
+ }
3188
+ return {
3189
+ status: "fail",
3190
+ reason: `${e}: \`import "react-native-gesture-handler"\` is not the first line`
3191
+ };
3192
+ }
3193
+ return { status: "skip", reason: "no entry file found" };
3194
+ }
3195
+ function checkFourWrapperChain(root, ownedAndPatchedFiles) {
3196
+ const wrappers = ["GestureHandlerRootView", "SafeAreaProvider", "CometChatThemeProvider", "CometChatProvider"];
3197
+ const hits = /* @__PURE__ */ new Set();
3198
+ for (const file of ownedAndPatchedFiles) {
3199
+ const content = readFileOrNull(p(root, file));
3200
+ if (!content) continue;
3201
+ for (const w of wrappers) {
3202
+ if (content.includes(w)) hits.add(w);
3203
+ }
3204
+ }
3205
+ const missing = wrappers.filter((w) => !hits.has(w));
3206
+ if (missing.length === 0) return { status: "pass" };
3207
+ return {
3208
+ status: "fail",
3209
+ reason: `missing wrapper(s): ${missing.join(", ")}`
3210
+ };
3211
+ }
3212
+ function checkHideReplyInThread(root, ownedAndPatchedFiles, ownedFiles) {
3213
+ for (const file of ownedAndPatchedFiles) {
3214
+ const content = readFileOrNull(p(root, file));
3215
+ if (content && content.includes("CometChatThreadHeader")) {
3216
+ return { status: "pass" };
3217
+ }
3218
+ }
3219
+ for (const file of ownedFiles) {
3220
+ const content = readFileOrNull(p(root, file));
3221
+ if (!content) continue;
3222
+ const lists = (content.match(/<\s*CometChatMessageList\b/g) ?? []).length;
3223
+ const flagged = (content.match(/hideReplyInThreadOption/g) ?? []).length;
3224
+ if (lists > flagged) {
3225
+ return {
3226
+ status: "fail",
3227
+ reason: `${file}: ${lists - flagged} MessageList without hideReplyInThreadOption`
3228
+ };
3229
+ }
3230
+ }
3231
+ return { status: "pass" };
3232
+ }
3233
+ function checkPodInstall(root) {
3234
+ if (!pathExists(p(root, "ios/Podfile"))) {
3235
+ return { status: "skip", reason: "no ios/Podfile (Expo managed or no iOS target)" };
3236
+ }
3237
+ if (pathExists(p(root, "ios/Podfile.lock"))) {
3238
+ return { status: "pass" };
3239
+ }
3240
+ return {
3241
+ status: "fail",
3242
+ reason: "ios/Podfile.lock missing \u2014 run `cd ios && pod install && cd ..`"
3243
+ };
3244
+ }
2893
3245
  function checkErrorUiVisible(root, ownedFiles) {
2894
3246
  for (const file of ownedFiles) {
2895
3247
  const content = readFileOrNull(p(root, file));
@@ -2927,7 +3279,15 @@ async function verify(args) {
2927
3279
  ...state2.files_owned,
2928
3280
  ...state2.files_patched.map((p2) => p2.path)
2929
3281
  ];
2930
- const checks = {
3282
+ const isRn2 = state2.framework === "expo" || state2.framework === "react-native";
3283
+ const checks = isRn2 ? {
3284
+ gesture_handler_line_1: checkGestureHandlerLine1(root),
3285
+ four_wrapper_chain_present: checkFourWrapperChain(root, ownedAndPatched),
3286
+ hide_reply_in_thread_option: checkHideReplyInThread(root, ownedAndPatched, state2.files_owned),
3287
+ no_auth_key_in_source: checkNoAuthKeyInSource(root, state2.files_owned),
3288
+ init_before_login: checkInitBeforeLogin(root, ownedAndPatched),
3289
+ ...state2.framework === "react-native" ? { pod_install_run: checkPodInstall(root) } : {}
3290
+ } : {
2931
3291
  css_variables_imported_once: checkCssVariablesImport(root, ownedAndPatched),
2932
3292
  init_before_login: checkInitBeforeLogin(root, ownedAndPatched),
2933
3293
  render_gated_on_login_resolve: checkRenderGatedOnLogin(root, ownedAndPatched),
@@ -2963,7 +3323,7 @@ function printHumanReadable5(r) {
2963
3323
  // src/commands/uninstall.ts
2964
3324
  init_state();
2965
3325
  init_fs();
2966
- import { rmSync as rmSync2, statSync as statSync3, rmdirSync, readdirSync as readdirSync3 } from "node:fs";
3326
+ import { rmSync as rmSync2, statSync as statSync3, rmdirSync, readdirSync as readdirSync4 } from "node:fs";
2967
3327
  import { dirname as dirname6, join as join9, resolve as resolve7 } from "node:path";
2968
3328
  var HELP6 = `
2969
3329
  cometchat uninstall \u2014 remove the CometChat integration cleanly
@@ -2997,7 +3357,7 @@ function projectPath6(args) {
2997
3357
  }
2998
3358
  function maybeRemoveEmptyDir(dir) {
2999
3359
  try {
3000
- const entries = readdirSync3(dir);
3360
+ const entries = readdirSync4(dir);
3001
3361
  if (entries.length === 0) {
3002
3362
  rmdirSync(dir);
3003
3363
  maybeRemoveEmptyDir(dirname6(dir));
@@ -3074,7 +3434,7 @@ async function uninstall(args) {
3074
3434
  try {
3075
3435
  const cometchatDir = p(root, ".cometchat");
3076
3436
  if (statSync3(cometchatDir).isDirectory()) {
3077
- const entries = readdirSync3(cometchatDir);
3437
+ const entries = readdirSync4(cometchatDir);
3078
3438
  if (entries.length === 0) rmdirSync(cometchatDir);
3079
3439
  }
3080
3440
  } catch {
@@ -3337,24 +3697,485 @@ function printHumanReadable7(r) {
3337
3697
  init_state();
3338
3698
  import { readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
3339
3699
  import { join as join10, resolve as resolve9 } from "node:path";
3700
+ init_config();
3701
+
3702
+ // src/utils/rn-production-templates.ts
3703
+ var HEADER = `/**
3704
+ * /cometchat-token \u2014 server-side endpoint that mints CometChat auth tokens.
3705
+ *
3706
+ * This is the production-mode replacement for client-side login({ uid }) +
3707
+ * Auth Key. The Auth Key NEVER leaves the server. The RN app calls this
3708
+ * endpoint to get a short-lived auth token, then passes it to
3709
+ * CometChatUIKit.login({ authToken }).
3710
+ *
3711
+ * Generated by \`cometchat production-auth\`. Customize freely:
3712
+ * - Replace the ?uid= query param with real auth: read the session token,
3713
+ * extract the user ID, map it to a CometChat UID.
3714
+ * - Add rate limiting if exposed publicly.
3715
+ * - Cache tokens per UID for short windows to reduce CometChat API load.
3716
+ *
3717
+ * Security: this file MUST NOT use EXPO_PUBLIC_ prefixes for the Auth Key.
3718
+ * The whole point is to keep the Auth Key out of the client bundle.
3719
+ */`;
3720
+ var EXPRESS_TEMPLATE = `${HEADER}
3721
+ import express from "express";
3722
+
3723
+ const app = express();
3724
+ app.use(express.json());
3725
+
3726
+ const COMETCHAT_APP_ID = process.env.COMETCHAT_APP_ID!;
3727
+ const COMETCHAT_REGION = process.env.COMETCHAT_REGION!;
3728
+ const COMETCHAT_AUTH_KEY = process.env.COMETCHAT_AUTH_KEY!;
3729
+
3730
+ app.get("/cometchat-token", async (req, res) => {
3731
+ // \u26A0 Replace this with your real auth: extract the session token, verify it,
3732
+ // and map your user record to a CometChat UID. Do not trust the query param.
3733
+ const uid = typeof req.query.uid === "string" ? req.query.uid : "cometchat-uid-1";
3734
+
3735
+ if (!COMETCHAT_APP_ID || !COMETCHAT_REGION || !COMETCHAT_AUTH_KEY) {
3736
+ return res.status(500).json({ error: "CometChat credentials not configured on the server" });
3737
+ }
3738
+
3739
+ try {
3740
+ const upstream = await fetch(
3741
+ \`https://\${COMETCHAT_APP_ID}.api-\${COMETCHAT_REGION}.cometchat.io/v3/users/\${encodeURIComponent(uid)}/auth_tokens\`,
3742
+ {
3743
+ method: "POST",
3744
+ headers: {
3745
+ "Content-Type": "application/json",
3746
+ appid: COMETCHAT_APP_ID,
3747
+ apikey: COMETCHAT_AUTH_KEY,
3748
+ },
3749
+ },
3750
+ );
3751
+ if (!upstream.ok) {
3752
+ const text = await upstream.text();
3753
+ return res.status(502).json({ error: \`CometChat token mint failed (\${upstream.status})\`, detail: text });
3754
+ }
3755
+ const body = (await upstream.json()) as { data?: { authToken?: string } };
3756
+ const authToken = body.data?.authToken;
3757
+ if (!authToken) {
3758
+ return res.status(502).json({ error: "CometChat API returned no authToken" });
3759
+ }
3760
+ return res.json({ authToken });
3761
+ } catch (err) {
3762
+ return res.status(500).json({ error: "Upstream CometChat request failed", detail: String(err) });
3763
+ }
3764
+ });
3765
+
3766
+ const PORT = Number(process.env.PORT ?? 8787);
3767
+ app.listen(PORT, () => console.log(\`cometchat-token endpoint listening on :\${PORT}\`));
3768
+ `;
3769
+ var HONO_TEMPLATE = `${HEADER}
3770
+ import { Hono } from "hono";
3771
+
3772
+ const app = new Hono();
3773
+
3774
+ app.get("/cometchat-token", async (c) => {
3775
+ const COMETCHAT_APP_ID = process.env.COMETCHAT_APP_ID!;
3776
+ const COMETCHAT_REGION = process.env.COMETCHAT_REGION!;
3777
+ const COMETCHAT_AUTH_KEY = process.env.COMETCHAT_AUTH_KEY!;
3778
+
3779
+ // \u26A0 Replace this with your real auth: extract the session token, verify it,
3780
+ // and map your user record to a CometChat UID. Do not trust the query param.
3781
+ const uid = c.req.query("uid") ?? "cometchat-uid-1";
3782
+
3783
+ if (!COMETCHAT_APP_ID || !COMETCHAT_REGION || !COMETCHAT_AUTH_KEY) {
3784
+ return c.json({ error: "CometChat credentials not configured on the server" }, 500);
3785
+ }
3786
+
3787
+ try {
3788
+ const upstream = await fetch(
3789
+ \`https://\${COMETCHAT_APP_ID}.api-\${COMETCHAT_REGION}.cometchat.io/v3/users/\${encodeURIComponent(uid)}/auth_tokens\`,
3790
+ {
3791
+ method: "POST",
3792
+ headers: {
3793
+ "Content-Type": "application/json",
3794
+ appid: COMETCHAT_APP_ID,
3795
+ apikey: COMETCHAT_AUTH_KEY,
3796
+ },
3797
+ },
3798
+ );
3799
+ if (!upstream.ok) {
3800
+ return c.json({ error: \`CometChat token mint failed (\${upstream.status})\` }, 502);
3801
+ }
3802
+ const body = (await upstream.json()) as { data?: { authToken?: string } };
3803
+ const authToken = body.data?.authToken;
3804
+ if (!authToken) return c.json({ error: "CometChat API returned no authToken" }, 502);
3805
+ return c.json({ authToken });
3806
+ } catch (err) {
3807
+ return c.json({ error: "Upstream CometChat request failed", detail: String(err) }, 500);
3808
+ }
3809
+ });
3810
+
3811
+ export default app;
3812
+ `;
3813
+ var FIREBASE_TEMPLATE = `${HEADER}
3814
+ import { onRequest } from "firebase-functions/v2/https";
3815
+ import { defineSecret } from "firebase-functions/params";
3816
+
3817
+ const COMETCHAT_APP_ID = defineSecret("COMETCHAT_APP_ID");
3818
+ const COMETCHAT_REGION = defineSecret("COMETCHAT_REGION");
3819
+ const COMETCHAT_AUTH_KEY = defineSecret("COMETCHAT_AUTH_KEY");
3820
+
3821
+ export const cometchatToken = onRequest(
3822
+ { secrets: [COMETCHAT_APP_ID, COMETCHAT_REGION, COMETCHAT_AUTH_KEY] },
3823
+ async (req, res) => {
3824
+ // \u26A0 Replace this with your real auth: verify Firebase ID token, map the
3825
+ // Firebase uid to a CometChat UID. Do not trust the query param.
3826
+ const uid = typeof req.query.uid === "string" ? req.query.uid : "cometchat-uid-1";
3827
+
3828
+ try {
3829
+ const upstream = await fetch(
3830
+ \`https://\${COMETCHAT_APP_ID.value()}.api-\${COMETCHAT_REGION.value()}.cometchat.io/v3/users/\${encodeURIComponent(uid)}/auth_tokens\`,
3831
+ {
3832
+ method: "POST",
3833
+ headers: {
3834
+ "Content-Type": "application/json",
3835
+ appid: COMETCHAT_APP_ID.value(),
3836
+ apikey: COMETCHAT_AUTH_KEY.value(),
3837
+ },
3838
+ },
3839
+ );
3840
+ if (!upstream.ok) {
3841
+ res.status(502).json({ error: \`CometChat token mint failed (\${upstream.status})\` });
3842
+ return;
3843
+ }
3844
+ const body = (await upstream.json()) as { data?: { authToken?: string } };
3845
+ const authToken = body.data?.authToken;
3846
+ if (!authToken) {
3847
+ res.status(502).json({ error: "CometChat API returned no authToken" });
3848
+ return;
3849
+ }
3850
+ res.json({ authToken });
3851
+ } catch (err) {
3852
+ res.status(500).json({ error: "Upstream CometChat request failed", detail: String(err) });
3853
+ }
3854
+ },
3855
+ );
3856
+ `;
3857
+ var NEXTJS_API_TEMPLATE = `${HEADER}
3858
+ import { NextRequest, NextResponse } from "next/server";
3859
+
3860
+ const COMETCHAT_APP_ID = process.env.COMETCHAT_APP_ID!;
3861
+ const COMETCHAT_REGION = process.env.COMETCHAT_REGION!;
3862
+ const COMETCHAT_AUTH_KEY = process.env.COMETCHAT_AUTH_KEY!;
3863
+
3864
+ export async function GET(request: NextRequest) {
3865
+ // \u26A0 Replace this with your real auth: read the session, extract the user ID,
3866
+ // then map it to a CometChat UID. Do not trust the query param in production.
3867
+ const uid = request.nextUrl.searchParams.get("uid") ?? "cometchat-uid-1";
3868
+
3869
+ if (!COMETCHAT_APP_ID || !COMETCHAT_REGION || !COMETCHAT_AUTH_KEY) {
3870
+ return NextResponse.json(
3871
+ { error: "CometChat credentials not configured on the server" },
3872
+ { status: 500 },
3873
+ );
3874
+ }
3875
+
3876
+ try {
3877
+ const upstream = await fetch(
3878
+ \`https://\${COMETCHAT_APP_ID}.api-\${COMETCHAT_REGION}.cometchat.io/v3/users/\${encodeURIComponent(uid)}/auth_tokens\`,
3879
+ {
3880
+ method: "POST",
3881
+ headers: {
3882
+ "Content-Type": "application/json",
3883
+ appid: COMETCHAT_APP_ID,
3884
+ apikey: COMETCHAT_AUTH_KEY,
3885
+ },
3886
+ },
3887
+ );
3888
+ if (!upstream.ok) {
3889
+ const text = await upstream.text();
3890
+ return NextResponse.json({ error: \`CometChat token mint failed (\${upstream.status})\`, detail: text }, { status: 502 });
3891
+ }
3892
+ const body = (await upstream.json()) as { data?: { authToken?: string } };
3893
+ const authToken = body.data?.authToken;
3894
+ if (!authToken) return NextResponse.json({ error: "CometChat API returned no authToken" }, { status: 502 });
3895
+ return NextResponse.json({ authToken });
3896
+ } catch (err) {
3897
+ return NextResponse.json({ error: "Upstream CometChat request failed", detail: String(err) }, { status: 500 });
3898
+ }
3899
+ }
3900
+ `;
3901
+ var USER_MGMT_HEADER = `/**
3902
+ * /cometchat-user \u2014 server-side user CRUD endpoint.
3903
+ *
3904
+ * Proxies to the CometChat REST API to keep your AUTH_KEY out of the client.
3905
+ * Wire this into your app's signup / profile-update / account-deletion flows
3906
+ * so every app user has a matching CometChat user.
3907
+ *
3908
+ * Generated by \`cometchat add-user-mgmt\`. Ships UNAUTHENTICATED \u2014 you MUST
3909
+ * add session checks (verify the caller's auth token / cookie) before
3910
+ * deploying. Anyone hitting this endpoint can create/update/delete arbitrary
3911
+ * CometChat users.
3912
+ */`;
3913
+ var EXPRESS_USER_TEMPLATE = `${USER_MGMT_HEADER}
3914
+ import express from "express";
3915
+
3916
+ const app = express();
3917
+ app.use(express.json());
3918
+
3919
+ const APP_ID = process.env.COMETCHAT_APP_ID!;
3920
+ const REGION = process.env.COMETCHAT_REGION!;
3921
+ const AUTH_KEY = process.env.COMETCHAT_AUTH_KEY!;
3922
+ const BASE = \`https://\${APP_ID}.api-\${REGION}.cometchat.io/v3/users\`;
3923
+
3924
+ async function cometchat(method: string, path: string, body?: unknown): Promise<Response> {
3925
+ return fetch(\`\${BASE}\${path}\`, {
3926
+ method,
3927
+ headers: { "Content-Type": "application/json", appid: APP_ID, apikey: AUTH_KEY },
3928
+ body: body ? JSON.stringify(body) : undefined,
3929
+ });
3930
+ }
3931
+
3932
+ app.post("/cometchat-user", async (req, res) => {
3933
+ // \u26A0 Add auth check here \u2014 verify the caller owns the uid they're creating.
3934
+ const { uid, name, avatar, metadata } = req.body ?? {};
3935
+ if (!uid || !name) return res.status(400).json({ error: "uid + name required" });
3936
+ const r = await cometchat("POST", "", { uid, name, avatar, metadata });
3937
+ return res.status(r.ok ? 201 : r.status).json(await r.json());
3938
+ });
3939
+
3940
+ app.patch("/cometchat-user/:uid", async (req, res) => {
3941
+ const r = await cometchat("PUT", \`/\${encodeURIComponent(req.params.uid)}\`, req.body);
3942
+ return res.status(r.ok ? 200 : r.status).json(await r.json());
3943
+ });
3944
+
3945
+ app.delete("/cometchat-user/:uid", async (req, res) => {
3946
+ const r = await cometchat("DELETE", \`/\${encodeURIComponent(req.params.uid)}?permanent=true\`);
3947
+ return res.status(r.ok ? 200 : r.status).json(r.ok ? { deleted: true } : await r.json());
3948
+ });
3949
+
3950
+ const PORT = Number(process.env.PORT ?? 8787);
3951
+ app.listen(PORT, () => console.log(\`cometchat-user endpoint listening on :\${PORT}\`));
3952
+ `;
3953
+ var HONO_USER_TEMPLATE = `${USER_MGMT_HEADER}
3954
+ import { Hono } from "hono";
3955
+
3956
+ const app = new Hono();
3957
+
3958
+ async function cometchat(method: string, path: string, body?: unknown): Promise<Response> {
3959
+ const APP_ID = process.env.COMETCHAT_APP_ID!;
3960
+ const REGION = process.env.COMETCHAT_REGION!;
3961
+ const AUTH_KEY = process.env.COMETCHAT_AUTH_KEY!;
3962
+ return fetch(\`https://\${APP_ID}.api-\${REGION}.cometchat.io/v3/users\${path}\`, {
3963
+ method,
3964
+ headers: { "Content-Type": "application/json", appid: APP_ID, apikey: AUTH_KEY },
3965
+ body: body ? JSON.stringify(body) : undefined,
3966
+ });
3967
+ }
3968
+
3969
+ app.post("/cometchat-user", async (c) => {
3970
+ // \u26A0 Add auth check here \u2014 verify the caller owns the uid they're creating.
3971
+ const { uid, name, avatar, metadata } = (await c.req.json()) ?? {};
3972
+ if (!uid || !name) return c.json({ error: "uid + name required" }, 400);
3973
+ const r = await cometchat("POST", "", { uid, name, avatar, metadata });
3974
+ return c.json(await r.json(), r.ok ? 201 : (r.status as 500));
3975
+ });
3976
+
3977
+ app.patch("/cometchat-user/:uid", async (c) => {
3978
+ const r = await cometchat("PUT", \`/\${encodeURIComponent(c.req.param("uid"))}\`, await c.req.json());
3979
+ return c.json(await r.json(), r.ok ? 200 : (r.status as 500));
3980
+ });
3981
+
3982
+ app.delete("/cometchat-user/:uid", async (c) => {
3983
+ const r = await cometchat("DELETE", \`/\${encodeURIComponent(c.req.param("uid"))}?permanent=true\`);
3984
+ return c.json(r.ok ? { deleted: true } : await r.json(), r.ok ? 200 : (r.status as 500));
3985
+ });
3986
+
3987
+ export default app;
3988
+ `;
3989
+ var FIREBASE_USER_TEMPLATE = `${USER_MGMT_HEADER}
3990
+ import { onRequest } from "firebase-functions/v2/https";
3991
+ import { defineSecret } from "firebase-functions/params";
3992
+
3993
+ const COMETCHAT_APP_ID = defineSecret("COMETCHAT_APP_ID");
3994
+ const COMETCHAT_REGION = defineSecret("COMETCHAT_REGION");
3995
+ const COMETCHAT_AUTH_KEY = defineSecret("COMETCHAT_AUTH_KEY");
3996
+
3997
+ async function cometchat(method: string, path: string, body?: unknown): Promise<Response> {
3998
+ return fetch(\`https://\${COMETCHAT_APP_ID.value()}.api-\${COMETCHAT_REGION.value()}.cometchat.io/v3/users\${path}\`, {
3999
+ method,
4000
+ headers: {
4001
+ "Content-Type": "application/json",
4002
+ appid: COMETCHAT_APP_ID.value(),
4003
+ apikey: COMETCHAT_AUTH_KEY.value(),
4004
+ },
4005
+ body: body ? JSON.stringify(body) : undefined,
4006
+ });
4007
+ }
4008
+
4009
+ export const cometchatUser = onRequest(
4010
+ { secrets: [COMETCHAT_APP_ID, COMETCHAT_REGION, COMETCHAT_AUTH_KEY] },
4011
+ async (req, res) => {
4012
+ // \u26A0 Add auth check: verify Firebase ID token, authorize the caller.
4013
+ const method = req.method;
4014
+ const segs = (req.path ?? "").split("/").filter(Boolean); // ["cometchat-user"] or ["cometchat-user", "<uid>"]
4015
+ const uid = segs.length >= 2 ? segs[1] : null;
4016
+
4017
+ if (method === "POST" && !uid) {
4018
+ const { uid: newUid, name, avatar, metadata } = (req.body ?? {}) as Record<string, unknown>;
4019
+ if (!newUid || !name) { res.status(400).json({ error: "uid + name required" }); return; }
4020
+ const r = await cometchat("POST", "", { uid: newUid, name, avatar, metadata });
4021
+ res.status(r.ok ? 201 : r.status).json(await r.json()); return;
4022
+ }
4023
+ if (method === "PATCH" && uid) {
4024
+ const r = await cometchat("PUT", \`/\${encodeURIComponent(uid)}\`, req.body);
4025
+ res.status(r.ok ? 200 : r.status).json(await r.json()); return;
4026
+ }
4027
+ if (method === "DELETE" && uid) {
4028
+ const r = await cometchat("DELETE", \`/\${encodeURIComponent(uid)}?permanent=true\`);
4029
+ res.status(r.ok ? 200 : r.status).json(r.ok ? { deleted: true } : await r.json()); return;
4030
+ }
4031
+ res.status(405).json({ error: "method/path not allowed" });
4032
+ },
4033
+ );
4034
+ `;
4035
+ var NEXTJS_USER_TEMPLATE = `${USER_MGMT_HEADER}
4036
+ import { NextRequest, NextResponse } from "next/server";
4037
+
4038
+ const APP_ID = process.env.COMETCHAT_APP_ID!;
4039
+ const REGION = process.env.COMETCHAT_REGION!;
4040
+ const AUTH_KEY = process.env.COMETCHAT_AUTH_KEY!;
4041
+ const BASE = \`https://\${APP_ID}.api-\${REGION}.cometchat.io/v3/users\`;
4042
+
4043
+ async function cometchat(method: string, path: string, body?: unknown): Promise<Response> {
4044
+ return fetch(\`\${BASE}\${path}\`, {
4045
+ method,
4046
+ headers: { "Content-Type": "application/json", appid: APP_ID, apikey: AUTH_KEY },
4047
+ body: body ? JSON.stringify(body) : undefined,
4048
+ });
4049
+ }
4050
+
4051
+ // POST /api/cometchat-user \u2014 create user. Wire into your signup flow.
4052
+ export async function POST(request: NextRequest) {
4053
+ // \u26A0 Add auth check here \u2014 verify the caller's session.
4054
+ const { uid, name, avatar, metadata } = await request.json();
4055
+ if (!uid || !name) return NextResponse.json({ error: "uid + name required" }, { status: 400 });
4056
+ const r = await cometchat("POST", "", { uid, name, avatar, metadata });
4057
+ return NextResponse.json(await r.json(), { status: r.ok ? 201 : r.status });
4058
+ }
4059
+
4060
+ // PATCH /api/cometchat-user?uid=<id> \u2014 update user.
4061
+ export async function PATCH(request: NextRequest) {
4062
+ const uid = request.nextUrl.searchParams.get("uid");
4063
+ if (!uid) return NextResponse.json({ error: "uid query param required" }, { status: 400 });
4064
+ const r = await cometchat("PUT", \`/\${encodeURIComponent(uid)}\`, await request.json());
4065
+ return NextResponse.json(await r.json(), { status: r.ok ? 200 : r.status });
4066
+ }
4067
+
4068
+ // DELETE /api/cometchat-user?uid=<id> \u2014 permanent user delete.
4069
+ export async function DELETE(request: NextRequest) {
4070
+ const uid = request.nextUrl.searchParams.get("uid");
4071
+ if (!uid) return NextResponse.json({ error: "uid query param required" }, { status: 400 });
4072
+ const r = await cometchat("DELETE", \`/\${encodeURIComponent(uid)}?permanent=true\`);
4073
+ return NextResponse.json(r.ok ? { deleted: true } : await r.json(), { status: r.ok ? 200 : r.status });
4074
+ }
4075
+ `;
4076
+ function rnUserMgmtTemplate(backend) {
4077
+ switch (backend) {
4078
+ case "express":
4079
+ return {
4080
+ filename: "cometchat-user.ts",
4081
+ content: EXPRESS_USER_TEMPLATE,
4082
+ deps: ["express"],
4083
+ runCommand: "npx tsx cometchat-user.ts",
4084
+ label: "Express user-mgmt endpoint"
4085
+ };
4086
+ case "hono":
4087
+ return {
4088
+ filename: "cometchat-user.ts",
4089
+ content: HONO_USER_TEMPLATE,
4090
+ deps: ["hono"],
4091
+ runCommand: "npx tsx cometchat-user.ts # or bun run",
4092
+ label: "Hono user-mgmt endpoint"
4093
+ };
4094
+ case "firebase-functions":
4095
+ return {
4096
+ filename: "cometchatUser.ts",
4097
+ content: FIREBASE_USER_TEMPLATE,
4098
+ deps: ["firebase-functions"],
4099
+ runCommand: "firebase deploy --only functions",
4100
+ label: "Firebase Functions user-mgmt endpoint"
4101
+ };
4102
+ case "nextjs-api":
4103
+ return {
4104
+ filename: "route.ts",
4105
+ content: NEXTJS_USER_TEMPLATE,
4106
+ deps: [],
4107
+ runCommand: null,
4108
+ label: "Next.js API route (drop into src/app/api/cometchat-user/route.ts)"
4109
+ };
4110
+ }
4111
+ }
4112
+ function rnBackendTemplate(backend) {
4113
+ switch (backend) {
4114
+ case "express":
4115
+ return {
4116
+ filename: "cometchat-token.ts",
4117
+ content: EXPRESS_TEMPLATE,
4118
+ deps: ["express"],
4119
+ runCommand: "npx tsx cometchat-token.ts",
4120
+ label: "Express server"
4121
+ };
4122
+ case "hono":
4123
+ return {
4124
+ filename: "cometchat-token.ts",
4125
+ content: HONO_TEMPLATE,
4126
+ deps: ["hono"],
4127
+ runCommand: "npx tsx cometchat-token.ts # or bun run, depending on your runtime",
4128
+ label: "Hono router (works with Bun, Cloudflare Workers, Node, Deno)"
4129
+ };
4130
+ case "firebase-functions":
4131
+ return {
4132
+ filename: "cometchatToken.ts",
4133
+ content: FIREBASE_TEMPLATE,
4134
+ deps: ["firebase-functions"],
4135
+ runCommand: "firebase deploy --only functions",
4136
+ label: "Firebase Cloud Functions (v2)"
4137
+ };
4138
+ case "nextjs-api":
4139
+ return {
4140
+ filename: "route.ts",
4141
+ content: NEXTJS_API_TEMPLATE,
4142
+ deps: [],
4143
+ runCommand: null,
4144
+ label: "Next.js App Router API route (add to an existing Next app under src/app/api/cometchat-token/route.ts)"
4145
+ };
4146
+ }
4147
+ }
4148
+ var RN_BACKENDS = ["express", "hono", "firebase-functions", "nextjs-api"];
4149
+
4150
+ // src/commands/production-auth.ts
3340
4151
  var HELP8 = `
3341
4152
  cometchat production-auth \u2014 upgrade dev integration to server-side tokens
3342
4153
 
3343
- Usage:
4154
+ Usage (web \u2014 nextjs / react-router / astro):
3344
4155
  cometchat production-auth [--path <p>] [--json]
3345
4156
 
4157
+ Usage (React Native \u2014 expo / react-native):
4158
+ cometchat production-auth --backend <express|hono|firebase-functions|nextjs-api>
4159
+ [--out-dir <path>] [--path <p>] [--json]
4160
+
3346
4161
  What this does:
3347
- Replaces client-side login(UID) + setAuthKey() with a server-side token
3348
- endpoint so your AUTH_KEY never reaches the browser. Creates the API
3349
- route file and auto-rewrites the canonical client login chain into a
3350
- fetch('/api/cometchat-token?uid=...') \u2192 loginWithAuthToken flow. If the
3351
- client file was customized, the auto-patch is skipped and manual steps
3352
- are surfaced. Supports nextjs, react-router, and astro.
4162
+ Web: replaces client-side login(UID) + setAuthKey() with a server-side
4163
+ token endpoint so your AUTH_KEY never reaches the browser. Creates
4164
+ the API route file and auto-rewrites the client login chain.
4165
+ RN: writes a standalone token endpoint (your choice of backend \u2014 Express,
4166
+ Hono, Firebase Functions, or a Next.js API route file you drop into
4167
+ an adjacent Next app). Does not patch the RN client \u2014 prints manual
4168
+ steps to update your CometChatProvider.
3353
4169
 
3354
4170
  Flags:
3355
- --path <p> Project root (defaults to cwd).
3356
- --json Machine-readable JSON output.
3357
- --help, -h Show this help.
4171
+ --backend <name> (RN only) Backend flavor. One of: express, hono,
4172
+ firebase-functions, nextjs-api.
4173
+ --out-dir <path> (RN only) Directory to write the endpoint file to
4174
+ (default: ./server for express/hono/nextjs-api,
4175
+ ./functions/src for firebase-functions).
4176
+ --path <p> Project root (defaults to cwd).
4177
+ --json Machine-readable JSON output.
4178
+ --help, -h Show this help.
3358
4179
  `;
3359
4180
  function isJsonMode9(args) {
3360
4181
  return args.flags.json === true || args.flags.json === "true";
@@ -3425,13 +4246,29 @@ async function productionAuth(args) {
3425
4246
  return 0;
3426
4247
  }
3427
4248
  const root = projectPath8(args);
3428
- if (!hasState(root)) {
4249
+ const stateFramework = hasState(root) ? readState(root)?.framework ?? null : null;
4250
+ const cfgFramework = readConfig(root)?.framework ?? null;
4251
+ const resolvedFramework = stateFramework ?? cfgFramework;
4252
+ if (!resolvedFramework) {
3429
4253
  const result2 = {
3430
4254
  status: "no-integration",
3431
4255
  framework: "",
3432
4256
  files_created: [],
3433
4257
  next_steps: [],
3434
- error: "API_ERROR: ERR_NO_INTEGRATION No integration found. Run `cometchat apply` first to create the dev integration, then run this command to upgrade to production-ready auth."
4258
+ error: "API_ERROR: ERR_NO_INTEGRATION No integration found. Run `cometchat apply` (web) or `/cometchat` (React Native) first to create the dev integration, then re-run this command."
4259
+ };
4260
+ return outputResult5(args, result2, 1);
4261
+ }
4262
+ if (resolvedFramework === "expo" || resolvedFramework === "react-native") {
4263
+ return productionAuthRn(args, root, resolvedFramework);
4264
+ }
4265
+ if (!hasState(root)) {
4266
+ const result2 = {
4267
+ status: "no-integration",
4268
+ framework: resolvedFramework,
4269
+ files_created: [],
4270
+ next_steps: [],
4271
+ error: "API_ERROR: ERR_NO_STATE Web production-auth requires state.json. Run `cometchat apply` first (or `cometchat state record` if your integration was hand-written)."
3435
4272
  };
3436
4273
  return outputResult5(args, result2, 1);
3437
4274
  }
@@ -3639,6 +4476,84 @@ function printHumanReadable8(r) {
3639
4476
  lines.push("");
3640
4477
  console.log(lines.join("\n"));
3641
4478
  }
4479
+ function productionAuthRn(args, root, framework) {
4480
+ const backendFlag = args.flags.backend;
4481
+ const outDirFlag = args.flags["out-dir"];
4482
+ if (typeof backendFlag !== "string" || !RN_BACKENDS.includes(backendFlag)) {
4483
+ const result2 = {
4484
+ status: "error",
4485
+ framework,
4486
+ files_created: [],
4487
+ next_steps: [],
4488
+ error: `React Native production-auth requires --backend <${RN_BACKENDS.join("|")}>. Example: \`cometchat production-auth --backend express --out-dir ./server\`. RN apps don't have a colocated backend, so you pick which flavor to scaffold.`
4489
+ };
4490
+ return outputResult5(args, result2, 1);
4491
+ }
4492
+ const backend = backendFlag;
4493
+ const tpl = rnBackendTemplate(backend);
4494
+ const defaultOutDir = backend === "firebase-functions" ? "functions/src" : backend === "nextjs-api" ? "src/app/api/cometchat-token" : "server";
4495
+ const outDir = typeof outDirFlag === "string" ? outDirFlag : defaultOutDir;
4496
+ const filePath = join10(outDir, tpl.filename);
4497
+ let writeResult;
4498
+ try {
4499
+ writeResult = applyWrites(root, [
4500
+ { type: "create", path: filePath, content: tpl.content, owned: true }
4501
+ ]);
4502
+ } catch (err) {
4503
+ return errorOut3(args, "RN production-auth apply failed: " + (err instanceof Error ? err.message : String(err)));
4504
+ }
4505
+ if (writeResult.files_skipped.length > 0) {
4506
+ const result2 = {
4507
+ status: "already-applied",
4508
+ framework,
4509
+ files_created: [],
4510
+ next_steps: [
4511
+ `Skipped: ${filePath} already exists. Delete it to regenerate with a different --backend.`
4512
+ ],
4513
+ error: `API_ERROR: ERR_ALREADY_APPLIED ${filePath} already exists.`
4514
+ };
4515
+ return outputResult5(args, result2, 0);
4516
+ }
4517
+ const depsStr = tpl.deps.length > 0 ? tpl.deps.join(" ") : null;
4518
+ const restart = framework === "expo" ? "npx expo start --clear" : "npm start -- --reset-cache";
4519
+ const next_steps = [
4520
+ `1. Backend deps: ${depsStr ? `\`npm install ${depsStr}\` in ${outDir.split("/")[0] ?? outDir}` : "(none \u2014 route file ships ready to deploy)"}.`,
4521
+ `2. Set server env vars (NO EXPO_PUBLIC_ prefix):`,
4522
+ ` COMETCHAT_APP_ID=<your-app-id>`,
4523
+ ` COMETCHAT_REGION=<us|eu|in>`,
4524
+ ` COMETCHAT_AUTH_KEY=<your-auth-key> # SERVER-ONLY, keep out of the RN bundle`,
4525
+ `3. Start the backend: ${tpl.runCommand ?? "(deploy per platform convention)"}`,
4526
+ `4. Remove \`EXPO_PUBLIC_COMETCHAT_AUTH_KEY\` (or bare COMETCHAT_AUTH_KEY for bare RN) from your RN .env \u2014 the client no longer needs it.`,
4527
+ `5. Client-side change in your CometChatProvider:`,
4528
+ ` - Remove \`.setAuthKey(...)\` from UIKitSettingsBuilder (keep setAppId + setRegion).`,
4529
+ ` - In your ensureLoggedIn() helper, replace \`login({ uid })\` with:`,
4530
+ ` const res = await fetch(\`\${API_BASE}/cometchat-token?uid=\${uid}\`);`,
4531
+ ` const { authToken } = await res.json();`,
4532
+ ` await CometChatUIKit.login({ authToken });`,
4533
+ `6. Restart Metro with \`${restart}\` and log in. The CometChat SDK never sees your Auth Key anymore.`,
4534
+ ` See cometchat-native-production skill for auth-provider integrations (Firebase Auth, Supabase, Clerk, Auth0) and token caching.`
4535
+ ];
4536
+ appendAuditEntry(root, {
4537
+ command: "production-auth",
4538
+ summary: `Scaffolded ${tpl.label} token endpoint at ${filePath}`,
4539
+ inputs: { framework, backend, "out-dir": outDir },
4540
+ decisions: {
4541
+ backend: `${tpl.label}. RN apps call out to a separate backend (not colocated), so the CLI scaffolds just the endpoint file; you wire the client-side change manually in CometChatProvider because the RN login flow structure varies across dispatchers.`,
4542
+ file: `Wrote ${filePath} (owned). Deps: ${depsStr ?? "none"}.`
4543
+ },
4544
+ files_patched: [filePath],
4545
+ next_actions: next_steps
4546
+ });
4547
+ const result = {
4548
+ status: "applied",
4549
+ framework,
4550
+ files_created: [filePath],
4551
+ client_patch: "n/a",
4552
+ // RN client-side is manual — too much structural variation to auto-patch
4553
+ next_steps
4554
+ };
4555
+ return outputResult5(args, result, 0);
4556
+ }
3642
4557
 
3643
4558
  // src/commands/apply-theme.ts
3644
4559
  init_state();
@@ -3803,6 +4718,75 @@ function buildOverrideBlock(theme) {
3803
4718
  }
3804
4719
  return { css: lines.join("\n") + "\n", variables: vars };
3805
4720
  }
4721
+ function isReactNativeFramework(framework) {
4722
+ return framework === "expo" || framework === "react-native";
4723
+ }
4724
+ function buildRnThemeSource(theme, useTs) {
4725
+ const vars = [];
4726
+ const lightColorLines = [];
4727
+ lightColorLines.push(` primary: "${theme.primaryColor}",`);
4728
+ vars.push("color.primary");
4729
+ if (theme.textColor) {
4730
+ lightColorLines.push(` textPrimary: "${theme.textColor}",`);
4731
+ vars.push("color.textPrimary");
4732
+ }
4733
+ if (theme.backgroundColor) {
4734
+ lightColorLines.push(` background1: "${theme.backgroundColor}",`);
4735
+ vars.push("color.background1");
4736
+ }
4737
+ const typographyLines = [];
4738
+ if (theme.fontFamily) {
4739
+ typographyLines.push(` body1: { fontFamily: "${theme.fontFamily}" },`);
4740
+ typographyLines.push(` heading1: { fontFamily: "${theme.fontFamily}" },`);
4741
+ vars.push("typography.body1.fontFamily");
4742
+ vars.push("typography.heading1.fontFamily");
4743
+ }
4744
+ const darkBlock = [];
4745
+ if (theme.darkMode) {
4746
+ darkBlock.push(" dark: {");
4747
+ darkBlock.push(" color: {");
4748
+ darkBlock.push(` primary: "${theme.primaryColor}",`);
4749
+ darkBlock.push(` background1: "#0A0A0A",`);
4750
+ darkBlock.push(` background2: "#1A1A1A",`);
4751
+ darkBlock.push(` background3: "#2A2A2A",`);
4752
+ darkBlock.push(` textPrimary: "#EDEDED",`);
4753
+ darkBlock.push(" },");
4754
+ darkBlock.push(" },");
4755
+ vars.push("dark.color.primary");
4756
+ vars.push("dark.color.background1-3");
4757
+ vars.push("dark.color.textPrimary");
4758
+ }
4759
+ const exportType = useTs ? ": Partial<CometChatTheme>" : "";
4760
+ const lines = [];
4761
+ lines.push("// CometChat theme \u2014 generated by `cometchat apply-theme`");
4762
+ lines.push("// Pass this to <CometChatThemeProvider theme={cometchatTheme}> in your app root.");
4763
+ lines.push("");
4764
+ if (useTs) {
4765
+ lines.push(`import type { CometChatTheme } from "@cometchat/chat-uikit-react-native";`);
4766
+ lines.push("");
4767
+ }
4768
+ lines.push(`export const cometchatTheme${exportType} = {`);
4769
+ lines.push(" light: {");
4770
+ lines.push(" color: {");
4771
+ lightColorLines.forEach((l) => lines.push(l));
4772
+ lines.push(" },");
4773
+ if (typographyLines.length > 0) {
4774
+ lines.push(" typography: {");
4775
+ typographyLines.forEach((l) => lines.push(l));
4776
+ lines.push(" },");
4777
+ }
4778
+ lines.push(" },");
4779
+ if (darkBlock.length > 0) {
4780
+ darkBlock.forEach((l) => lines.push(l));
4781
+ }
4782
+ lines.push("};");
4783
+ lines.push("");
4784
+ return { content: lines.join("\n"), variables: vars };
4785
+ }
4786
+ function rnThemePath(root) {
4787
+ const isTs = pathExists(p(root, "tsconfig.json"));
4788
+ return isTs ? "providers/CometChatTheme.ts" : "providers/CometChatTheme.js";
4789
+ }
3806
4790
  function buildThemeOp(framework, cssBlock, root) {
3807
4791
  const targets = {
3808
4792
  reactjs: ["src/index.css", "src/main.css", "src/styles.css"],
@@ -3904,6 +4888,54 @@ async function applyTheme(args) {
3904
4888
  `Error: pass either --preset <name> or --primary-color <hex>. Available presets: ${listPresets().join(", ")}. Or use --primary-color #6852D6 with optional --text-color, --background-color, --font-family, --border-radius, --dark-mode.`
3905
4889
  );
3906
4890
  }
4891
+ if (isReactNativeFramework(framework)) {
4892
+ const themePath = rnThemePath(root);
4893
+ const useTs = themePath.endsWith(".ts");
4894
+ const { content, variables: rnVariables } = buildRnThemeSource(theme, useTs);
4895
+ let writeResult2;
4896
+ try {
4897
+ writeResult2 = applyWrites(root, [
4898
+ { type: "create", path: themePath, content, owned: true }
4899
+ ]);
4900
+ } catch (err) {
4901
+ return errorOut4(args, "Theme apply failed: " + (err instanceof Error ? err.message : String(err)));
4902
+ }
4903
+ const wasSkipped2 = writeResult2.files_skipped.length > 0;
4904
+ const wireHint = `Wire it in: import { cometchatTheme } from "./${themePath.replace(/\.(ts|js)$/, "")}"; then <CometChatThemeProvider theme={cometchatTheme}>\u2026</CometChatThemeProvider>`;
4905
+ const result2 = {
4906
+ status: "applied",
4907
+ framework,
4908
+ file_modified: themePath,
4909
+ variables_applied: rnVariables,
4910
+ next_steps: [
4911
+ wasSkipped2 ? `Skipped: ${themePath} already exists. Delete it to regenerate.` : `Theme object written to ${themePath}.`,
4912
+ wireHint,
4913
+ framework === "expo" ? "Restart Metro with `npx expo start --clear` to pick up the new theme." : "Restart Metro with `npm start -- --reset-cache` to pick up the new theme.",
4914
+ "To remove the theme, delete the file and drop the `theme` prop on CometChatThemeProvider."
4915
+ ]
4916
+ };
4917
+ if (!wasSkipped2) {
4918
+ const presetFlag = getStringFlag3(args, "preset");
4919
+ appendAuditEntry(root, {
4920
+ command: "apply-theme",
4921
+ summary: presetFlag ? `Applied "${presetFlag}" theme preset to ${framework} integration` : `Applied custom theme overrides to ${framework} integration`,
4922
+ inputs: {
4923
+ framework,
4924
+ ...presetFlag ? { preset: presetFlag } : {},
4925
+ ...theme.primaryColor ? { "primary-color": theme.primaryColor } : {},
4926
+ ...theme.darkMode ? { "dark-mode": true } : {}
4927
+ },
4928
+ decisions: {
4929
+ target: `${framework}: wrote theme module at ${themePath} for use with <CometChatThemeProvider theme={...}>. RN uses a JS theme object, not CSS.`,
4930
+ ...presetFlag ? { preset: `picked the "${presetFlag}" preset bundle (primary + text + background + font + dark-mode)` } : {},
4931
+ variables_set: rnVariables.join(", ")
4932
+ },
4933
+ files_patched: [themePath],
4934
+ next_actions: result2.next_steps
4935
+ });
4936
+ }
4937
+ return outputResult6(args, result2, 0);
4938
+ }
3907
4939
  const { css, variables } = buildOverrideBlock(theme);
3908
4940
  const { op, targetPath, specialHint } = buildThemeOp(framework, css, root);
3909
4941
  if (!op || !targetPath) {
@@ -4037,6 +5069,11 @@ async function hasSecretTool() {
4037
5069
  }
4038
5070
  }
4039
5071
  async function detectBackend() {
5072
+ const override = process.env.CC_AUTH_BACKEND;
5073
+ if (override === "file") return "file";
5074
+ if (override === "keychain-macos") return "keychain-macos";
5075
+ if (override === "keychain-linux") return "keychain-linux";
5076
+ if (override === "keychain-windows") return "keychain-windows";
4040
5077
  if (process.platform === "darwin") return "keychain-macos";
4041
5078
  if (process.platform === "win32") return "keychain-windows";
4042
5079
  if (process.platform === "linux" && await hasSecretTool()) return "keychain-linux";
@@ -4343,8 +5380,19 @@ async function listApps(host, token) {
4343
5380
  const name = String(entry.name ?? id);
4344
5381
  const region = String(entry.region ?? "us");
4345
5382
  const plan = typeof entry.plan === "string" ? entry.plan : void 0;
4346
- const base = { id, name, region };
4347
- return plan ? { ...base, plan } : base;
5383
+ const meta = entry.metadata && typeof entry.metadata === "object" ? entry.metadata : {};
5384
+ const industry = typeof meta.industry === "string" ? meta.industry : void 0;
5385
+ const technology = typeof meta.technology === "string" ? meta.technology : void 0;
5386
+ const product = typeof meta.product === "string" ? meta.product : void 0;
5387
+ const createdAtRaw = entry.createdAt ?? entry.created_at;
5388
+ const createdAt = typeof createdAtRaw === "number" ? createdAtRaw : typeof createdAtRaw === "string" ? Number(new Date(createdAtRaw)) || void 0 : void 0;
5389
+ const out = { id, name, region };
5390
+ if (plan) out.plan = plan;
5391
+ if (industry) out.industry = industry;
5392
+ if (technology) out.technology = technology;
5393
+ if (product) out.product = product;
5394
+ if (createdAt !== void 0) out.createdAt = createdAt;
5395
+ return out;
4348
5396
  });
4349
5397
  }
4350
5398
  async function getAppCredentials(host, token, appId) {
@@ -4371,28 +5419,105 @@ async function getAppCredentials(host, token, appId) {
4371
5419
  }
4372
5420
  return { appId: id, authKey, region };
4373
5421
  }
4374
- async function listInstalledExtensions(host, token, appId) {
4375
- const res = await request(
4376
- host,
4377
- `/apps/${encodeURIComponent(appId)}/extensions?per_page=50`,
4378
- { method: "GET", headers: { Authorization: `Bearer ${token}` } }
4379
- );
5422
+ async function getCurrentUser(host, token) {
5423
+ const res = await request(host, "/me", {
5424
+ method: "GET",
5425
+ headers: { Authorization: `Bearer ${token}` }
5426
+ });
5427
+ const body = await parseBody(res);
5428
+ if (!res.ok) {
5429
+ const { code, message } = extractErrorCode(body);
5430
+ if (res.status === 401) throw new Error("AUTH_FAILED");
5431
+ throw new Error(`API_ERROR: ${code} ${message}`);
5432
+ }
5433
+ const data = body?.data;
5434
+ if (!data || typeof data !== "object") {
5435
+ throw new Error(`API_ERROR: UNEXPECTED_SHAPE ${JSON.stringify(body)}`);
5436
+ }
5437
+ return data;
5438
+ }
5439
+ async function getCurrentUserWithLastApp(host, token) {
5440
+ const user = await getCurrentUser(host, token);
5441
+ let last_app = null;
5442
+ try {
5443
+ const apps = await listApps(host, token);
5444
+ if (apps.length > 0) {
5445
+ const sorted = [...apps].sort((a, b) => {
5446
+ const aT = a.createdAt ?? 0;
5447
+ const bT = b.createdAt ?? 0;
5448
+ return bT - aT;
5449
+ });
5450
+ last_app = sorted[0];
5451
+ }
5452
+ } catch {
5453
+ }
5454
+ return { ...user, last_app };
5455
+ }
5456
+ async function listInstalledExtensions(host, token, appId) {
5457
+ const res = await request(
5458
+ host,
5459
+ `/apps/${encodeURIComponent(appId)}/extensions?per_page=50`,
5460
+ { method: "GET", headers: { Authorization: `Bearer ${token}` } }
5461
+ );
5462
+ const body = await parseBody(res);
5463
+ if (!res.ok) {
5464
+ const { code, message } = extractErrorCode(body);
5465
+ if (res.status === 401) throw new Error("AUTH_FAILED");
5466
+ throw new Error(`API_ERROR: ${code} ${message}`);
5467
+ }
5468
+ const data = body?.data;
5469
+ if (!Array.isArray(data)) return [];
5470
+ return data.map((e) => e);
5471
+ }
5472
+ async function toggleExtension(host, token, appId, extensionId, action) {
5473
+ const payload = action === "enable" ? { enabled: [extensionId] } : { disabled: [extensionId] };
5474
+ const res = await request(host, `/apps/${encodeURIComponent(appId)}/extensions`, {
5475
+ method: "POST",
5476
+ headers: { Authorization: `Bearer ${token}` },
5477
+ body: JSON.stringify(payload)
5478
+ });
5479
+ if (!res.ok) {
5480
+ const body = await parseBody(res);
5481
+ const { code, message } = extractErrorCode(body);
5482
+ if (res.status === 401) throw new Error("AUTH_FAILED");
5483
+ throw new Error(`API_ERROR: ${code} ${message}`);
5484
+ }
5485
+ }
5486
+ async function toggleAiFeature(host, token, appId, featureKey, action) {
5487
+ const path = `/apps/${encodeURIComponent(appId)}/features/ai.${encodeURIComponent(featureKey)}/enabled`;
5488
+ const res = await request(host, path, {
5489
+ method: action === "enable" ? "POST" : "DELETE",
5490
+ headers: { Authorization: `Bearer ${token}` },
5491
+ body: action === "enable" ? "{}" : void 0
5492
+ });
5493
+ if (!res.ok) {
5494
+ const body = await parseBody(res);
5495
+ const { code, message } = extractErrorCode(body);
5496
+ if (res.status === 401) throw new Error("AUTH_FAILED");
5497
+ throw new Error(`API_ERROR: ${code} ${message}`);
5498
+ }
5499
+ }
5500
+ async function getAiSettings(host, token, appId) {
5501
+ const res = await request(host, `/apps/${encodeURIComponent(appId)}/ai/settings`, {
5502
+ method: "GET",
5503
+ headers: { Authorization: `Bearer ${token}` }
5504
+ });
4380
5505
  const body = await parseBody(res);
4381
5506
  if (!res.ok) {
4382
5507
  const { code, message } = extractErrorCode(body);
4383
5508
  if (res.status === 401) throw new Error("AUTH_FAILED");
5509
+ if (res.status === 404) return null;
4384
5510
  throw new Error(`API_ERROR: ${code} ${message}`);
4385
5511
  }
4386
5512
  const data = body?.data;
4387
- if (!Array.isArray(data)) return [];
4388
- return data.map((e) => e);
5513
+ if (!data || typeof data !== "object") return null;
5514
+ return data;
4389
5515
  }
4390
- async function toggleExtension(host, token, appId, extensionId, action) {
4391
- const payload = action === "enable" ? { enabled: [extensionId] } : { disabled: [extensionId] };
4392
- const res = await request(host, `/apps/${encodeURIComponent(appId)}/extensions`, {
4393
- method: "POST",
5516
+ async function updateAiSettings(host, token, appId, settings) {
5517
+ const res = await request(host, `/apps/${encodeURIComponent(appId)}/ai/settings`, {
5518
+ method: "PUT",
4394
5519
  headers: { Authorization: `Bearer ${token}` },
4395
- body: JSON.stringify(payload)
5520
+ body: JSON.stringify(settings)
4396
5521
  });
4397
5522
  if (!res.ok) {
4398
5523
  const body = await parseBody(res);
@@ -4477,7 +5602,20 @@ function loadCatalog() {
4477
5602
  cachedCatalog = JSON.parse(readFileSync9(path, "utf8"));
4478
5603
  return cachedCatalog;
4479
5604
  }
4480
- function nextStepsForFeature(feature) {
5605
+ function resolveFramework(root) {
5606
+ if (hasState(root)) {
5607
+ const state2 = readState(root);
5608
+ if (state2?.framework) return state2.framework;
5609
+ }
5610
+ const cfg = readConfig(root);
5611
+ if (cfg?.framework) return cfg.framework;
5612
+ return "reactjs";
5613
+ }
5614
+ function isRn(framework) {
5615
+ return framework === "expo" || framework === "react-native";
5616
+ }
5617
+ function nextStepsForFeature(feature, framework = "reactjs") {
5618
+ const rn = isRn(framework);
4481
5619
  switch (feature.type) {
4482
5620
  case "default":
4483
5621
  return [
@@ -4486,53 +5624,96 @@ function nextStepsForFeature(feature) {
4486
5624
  feature.requires_dashboard_setup ? "Some features (like moderation) need rules configured in the CometChat dashboard before they activate. Check the dashboard." : "No code changes needed \u2014 it's already there.",
4487
5625
  ...feature.docs_topic ? [`For details, query the docs MCP for "${feature.docs_topic}" or visit https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}`] : []
4488
5626
  ];
4489
- case "dashboard-toggle": {
5627
+ case "extension": {
4490
5628
  const docsUrl = feature.docs_topic ? `https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}` : "https://www.cometchat.com/docs/ui-kit/react/extensions";
4491
5629
  const lines = [];
4492
5630
  if (feature.auto_wired_in_uikit) {
4493
5631
  lines.push(
4494
- `${feature.name} is in the UI Kit's defaultExtensions[] \u2014 its UI decorator is attached automatically by initiateAfterLogin(). The only thing you need to do is flip the dashboard toggle.`,
5632
+ `${feature.name} is in the UI Kit's defaultExtensions[] \u2014 its UI decorator is attached automatically by initiateAfterLogin().`,
4495
5633
  "",
4496
5634
  "Steps to enable:",
4497
- ` 1. Go to https://app.cometchat.com and select your app`,
4498
- ` 2. Navigate to: ${feature.dashboard_path ?? "Extensions \u2192 " + feature.name}`,
4499
- ` (this is a hint \u2014 if the dashboard UI has changed, see the canonical docs link below)`,
4500
- ` 3. Toggle the feature on (and configure any required values)`,
4501
- ` 4. Refresh your dev server \u2014 no code changes required`,
5635
+ ` 1. Run \`cometchat apply-feature ${feature.id}\` \u2014 the CLI flips the dashboard toggle via API.`,
5636
+ ` 2. Refresh your dev server \u2014 no code changes required.`,
4502
5637
  "",
4503
- ` \u{1F4D6} Canonical docs (always-current \u2014 use this if the dashboard navigation above doesn't match what you see):`,
4504
- ` ${docsUrl}`,
4505
- ` For exact, up-to-date integration steps, query the cometchat-docs MCP for "${feature.id}".`
5638
+ ` Manual fallback (if API isn't available): https://app.cometchat.com \u2192 ${feature.dashboard_path ?? "Extensions \u2192 " + feature.name}`,
5639
+ ` \u{1F4D6} Canonical docs: ${docsUrl}`
4506
5640
  );
4507
5641
  } else {
4508
5642
  lines.push(
4509
5643
  `${feature.name} is a dashboard-toggle extension that ALSO requires explicit registration in your UIKitSettingsBuilder. The UI Kit only auto-attaches the 7 extensions in defaultExtensions[]; this one is not in that list.`,
4510
5644
  "",
4511
5645
  "Steps to enable:",
4512
- ` 1. Go to https://app.cometchat.com \u2192 ${feature.dashboard_path ?? "Extensions \u2192 " + feature.name}, toggle on`,
4513
- ` (dashboard hint \u2014 see canonical docs link below if the navigation has changed)`,
5646
+ ` 1. Run \`cometchat apply-feature ${feature.id}\` \u2014 the CLI flips the dashboard toggle via API.`,
4514
5647
  ` 2. Register the extension via UIKitSettingsBuilder.setExtensions([...]) in your CometChat init.`,
4515
5648
  ` For the exact import path + extension class name + builder syntax, query the cometchat-docs MCP for "${feature.id}" \u2014 DO NOT invent the API from memory.`,
4516
- ` 3. Restart your dev server`,
5649
+ ` 3. Restart your dev server.`,
4517
5650
  "",
4518
5651
  ` \u{1F4D6} Canonical docs: ${docsUrl}`
4519
5652
  );
4520
5653
  }
4521
5654
  return lines;
4522
5655
  }
4523
- case "package-install":
5656
+ case "ai-feature": {
5657
+ const docsUrl = feature.docs_topic ? `https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}` : "https://www.cometchat.com/docs/ui-kit/react/ai-features";
4524
5658
  return [
5659
+ `${feature.name} is an AI feature that requires an OpenAI API key on the app's AI settings.`,
5660
+ "",
5661
+ "Steps to enable:",
5662
+ ` 1. Run \`cometchat apply-feature ${feature.id} --openai-key sk-...\` \u2014 the CLI stores the key and flips the AI toggle via API.`,
5663
+ ` 2. Refresh your dev server.`,
5664
+ "",
5665
+ ` Get an OpenAI key: https://platform.openai.com/api-keys`,
5666
+ ` \u{1F4D6} Canonical docs: ${docsUrl}`
5667
+ ];
5668
+ }
5669
+ case "dashboard-only":
5670
+ return [
5671
+ `${feature.name} requires third-party config (API key, webhook, etc.) that only you can provide. The CLI cannot automate this.`,
5672
+ "",
5673
+ "Steps to enable:",
5674
+ ` 1. Open https://app.cometchat.com \u2192 ${feature.dashboard_path ?? "Extensions \u2192 " + feature.name}`,
5675
+ ` 2. Enter the required configuration values.`,
5676
+ ` 3. Toggle the feature on; refresh your dev server.`,
5677
+ ...feature.docs_topic ? ["", ` \u{1F4D6} Canonical docs: https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}`] : []
5678
+ ];
5679
+ case "package-install": {
5680
+ const pkg = rn && feature.package_native ? feature.package_native : feature.package;
5681
+ const peers = rn ? feature.package_native_peers ?? [] : [];
5682
+ const installCmd = framework === "expo" ? `npx expo install ${[pkg, ...peers].filter(Boolean).join(" ")}` : `npm install ${[pkg, ...peers].filter(Boolean).join(" ")}`;
5683
+ const restartCmd = framework === "expo" ? "npx expo start --clear" : rn ? "npm start -- --reset-cache" : "npm run dev";
5684
+ const docsPath = rn ? "react-native" : "react";
5685
+ const baseSteps = [
4525
5686
  `${feature.name} requires installing an additional npm package.`,
4526
5687
  "",
4527
5688
  "Steps to enable:",
4528
- ` 1. Install the package:`,
4529
- ` npm install ${feature.package}`,
4530
- ` 2. Restart your dev server. After the next login, the UI Kit's initiateAfterLogin() automatically calls enableCalling(), which detects the calls SDK and wires up the default CallingExtension. No manual setExtensions() or setCallsExtension() needed for the default behavior.`,
4531
- ` 3. Call buttons appear in CometChatMessageHeader; incoming calls render via the global call listener.`,
4532
- ...feature.components && feature.components.length > 0 ? [` 4. Components automatically enabled: ${feature.components.join(", ")}`] : [],
4533
- ` Note: to customize the calling UI (custom CallingExtension, group calls config, recording), pass your own instance via UIKitSettingsBuilder.setCallsExtension(new CallingExtension({...})) before init.`,
4534
- ...feature.docs_topic ? [` Docs: https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}`] : []
5689
+ ` 1. Install:`,
5690
+ ` ${installCmd}`
4535
5691
  ];
5692
+ if (framework === "react-native") {
5693
+ baseSteps.push(` 2. Run \`cd ios && pod install && cd ..\` to link the native modules.`);
5694
+ baseSteps.push(` 3. Restart Metro with \`${restartCmd}\`. After the next login, the UI Kit's initiateAfterLogin() auto-wires the CometChat calling listeners.`);
5695
+ baseSteps.push(` 4. Call buttons appear in CometChatMessageHeader; incoming calls render via the CometChatIncomingCall listener.`);
5696
+ } else if (framework === "expo") {
5697
+ baseSteps.push(` 2. Run \`npx expo prebuild --clean\` (calling requires a dev build \u2014 Expo Go cannot load WebRTC).`);
5698
+ baseSteps.push(` 3. Restart Metro with \`${restartCmd}\`. After the next login, the UI Kit's initiateAfterLogin() auto-wires the CometChat calling listeners.`);
5699
+ baseSteps.push(` 4. Call buttons appear in CometChatMessageHeader; incoming calls render via the CometChatIncomingCall listener.`);
5700
+ } else {
5701
+ baseSteps.push(` 2. Restart your dev server (\`${restartCmd}\`). After the next login, the UI Kit's initiateAfterLogin() automatically calls enableCalling(), which detects the calls SDK and wires up the default CallingExtension. No manual setExtensions() or setCallsExtension() needed for the default behavior.`);
5702
+ baseSteps.push(` 3. Call buttons appear in CometChatMessageHeader; incoming calls render via the global call listener.`);
5703
+ }
5704
+ if (feature.components && feature.components.length > 0) {
5705
+ baseSteps.push(` Components automatically enabled: ${feature.components.join(", ")}`);
5706
+ }
5707
+ if (rn) {
5708
+ baseSteps.push(` Note: on React Native you customize calling UI via custom view templates (see cometchat-native-features skill \xA7 Calls).`);
5709
+ } else {
5710
+ baseSteps.push(` Note: to customize the calling UI (custom CallingExtension, group calls config, recording), pass your own instance via UIKitSettingsBuilder.setCallsExtension(new CallingExtension({...})) before init.`);
5711
+ }
5712
+ if (feature.docs_topic) {
5713
+ baseSteps.push(` Docs: https://www.cometchat.com/docs/ui-kit/${docsPath}/${feature.docs_topic}`);
5714
+ }
5715
+ return baseSteps;
5716
+ }
4536
5717
  case "component-swap":
4537
5718
  return [
4538
5719
  `${feature.name} is enabled by replacing one component with a variant.`,
@@ -4612,7 +5793,7 @@ async function featuresToggle(args, featureName, action) {
4612
5793
  if (json) {
4613
5794
  console.log(JSON.stringify({
4614
5795
  ...result2,
4615
- available: catalog.features.filter((f) => f.type === "dashboard-toggle").map((f) => f.id)
5796
+ available: catalog.features.filter((f) => f.type === "extension").map((f) => f.id)
4616
5797
  }, null, 2));
4617
5798
  } else {
4618
5799
  console.error(`\u2717 Feature "${featureName}" not found.`);
@@ -4620,11 +5801,12 @@ async function featuresToggle(args, featureName, action) {
4620
5801
  }
4621
5802
  return 1;
4622
5803
  }
4623
- if (match.type !== "dashboard-toggle") {
5804
+ if (match.type !== "extension") {
5805
+ const hint = match.type === "ai-feature" ? `Use \`cometchat apply-feature ${match.id} --openai-key <sk-\u2026>\` to enable AI features.` : `Run \`cometchat features info ${match.id}\` for the right instructions.`;
4624
5806
  const result2 = {
4625
5807
  status: "unsupported-type",
4626
5808
  feature: match,
4627
- error: `"${match.name}" is type "${match.type}" \u2014 not a dashboard toggle. Run \`cometchat features info ${match.id}\` for the right instructions.`
5809
+ error: `"${match.name}" is type "${match.type}" \u2014 not a dashboard toggle. ${hint}`
4628
5810
  };
4629
5811
  if (json) {
4630
5812
  console.log(JSON.stringify(result2, null, 2));
@@ -4765,7 +5947,9 @@ function featuresList(args) {
4765
5947
  }
4766
5948
  const order = [
4767
5949
  "default",
4768
- "dashboard-toggle",
5950
+ "extension",
5951
+ "ai-feature",
5952
+ "dashboard-only",
4769
5953
  "package-install",
4770
5954
  "component-swap"
4771
5955
  ];
@@ -4811,10 +5995,12 @@ function featuresInfo(args, name) {
4811
5995
  }
4812
5996
  return 1;
4813
5997
  }
5998
+ const root = projectPath10(args);
5999
+ const framework = resolveFramework(root);
4814
6000
  const result = {
4815
6001
  status: "found",
4816
6002
  feature: fuzzy,
4817
- next_steps: nextStepsForFeature(fuzzy)
6003
+ next_steps: nextStepsForFeature(fuzzy, framework)
4818
6004
  };
4819
6005
  if (isJsonMode11(args)) {
4820
6006
  console.log(JSON.stringify(result, null, 2));
@@ -4873,8 +6059,11 @@ function projectPath11(args) {
4873
6059
  if (typeof fromFlag === "string") return resolve12(fromFlag);
4874
6060
  return resolve12(process.cwd());
4875
6061
  }
4876
- function runQuickVerify(root, ownedFiles, patchedFiles = []) {
6062
+ function runQuickVerify(root, ownedFiles, patchedFiles = [], framework = null) {
4877
6063
  if (ownedFiles.length === 0) return { status: "skip", failed: [] };
6064
+ if (framework === "expo" || framework === "react-native") {
6065
+ return runQuickVerifyRn(root, ownedFiles, patchedFiles, framework);
6066
+ }
4878
6067
  const failed = [];
4879
6068
  const HEX_KEY = /[a-f0-9]{32,}/;
4880
6069
  const cssCandidates = /* @__PURE__ */ new Set([
@@ -4917,6 +6106,69 @@ function runQuickVerify(root, ownedFiles, patchedFiles = []) {
4917
6106
  if (!errorUiFound) failed.push("error_ui_visible_on_failure");
4918
6107
  return { status: failed.length === 0 ? "pass" : "fail", failed };
4919
6108
  }
6109
+ function runQuickVerifyRn(root, ownedFiles, patchedFiles, framework) {
6110
+ const failed = [];
6111
+ const HEX_KEY = /[a-f0-9]{32,}/;
6112
+ const allFiles = /* @__PURE__ */ new Set([...ownedFiles, ...patchedFiles]);
6113
+ const entryCandidates = ["index.js", "index.ts", "App.tsx", "App.jsx", "app/_layout.tsx", "app/_layout.js"];
6114
+ let gestureHandlerLine1 = false;
6115
+ for (const e of entryCandidates) {
6116
+ if (!pathExists(p(root, e))) continue;
6117
+ const content = readFileOrNull(p(root, e));
6118
+ if (!content) continue;
6119
+ const firstLine = content.split(/\r?\n/)[0]?.trim() ?? "";
6120
+ if (/^import\s+['"]react-native-gesture-handler['"]/.test(firstLine)) {
6121
+ gestureHandlerLine1 = true;
6122
+ break;
6123
+ }
6124
+ }
6125
+ if (!gestureHandlerLine1) failed.push("gesture_handler_line_1");
6126
+ const wrappers = ["GestureHandlerRootView", "SafeAreaProvider", "CometChatThemeProvider", "CometChatProvider"];
6127
+ const wrapperHits = /* @__PURE__ */ new Set();
6128
+ for (const file of allFiles) {
6129
+ const content = readFileOrNull(p(root, file));
6130
+ if (!content) continue;
6131
+ for (const w of wrappers) {
6132
+ if (content.includes(w)) wrapperHits.add(w);
6133
+ }
6134
+ }
6135
+ if (wrapperHits.size < wrappers.length) failed.push("four_wrapper_chain_present");
6136
+ let hasThreadPanel = false;
6137
+ for (const file of allFiles) {
6138
+ const content = readFileOrNull(p(root, file));
6139
+ if (content && content.includes("CometChatThreadHeader")) {
6140
+ hasThreadPanel = true;
6141
+ break;
6142
+ }
6143
+ }
6144
+ if (!hasThreadPanel) {
6145
+ for (const file of ownedFiles) {
6146
+ const content = readFileOrNull(p(root, file));
6147
+ if (!content) continue;
6148
+ const lists = (content.match(/<\s*CometChatMessageList\b/g) ?? []).length;
6149
+ const flagged = (content.match(/hideReplyInThreadOption/g) ?? []).length;
6150
+ if (lists > flagged) {
6151
+ failed.push("hide_reply_in_thread_option");
6152
+ break;
6153
+ }
6154
+ }
6155
+ }
6156
+ for (const file of ownedFiles) {
6157
+ const content = readFileOrNull(p(root, file));
6158
+ if (!content) continue;
6159
+ const m = content.match(HEX_KEY);
6160
+ if (m && m[0].length >= 40) {
6161
+ failed.push("no_auth_key_in_source");
6162
+ break;
6163
+ }
6164
+ }
6165
+ if (framework === "react-native" && pathExists(p(root, "ios/Podfile"))) {
6166
+ if (!pathExists(p(root, "ios/Podfile.lock"))) {
6167
+ failed.push("pod_install_run");
6168
+ }
6169
+ }
6170
+ return { status: failed.length === 0 ? "pass" : "fail", failed };
6171
+ }
4920
6172
  function buildIssues(args) {
4921
6173
  const issues = [];
4922
6174
  if (!args.hasIntegration) {
@@ -4964,6 +6216,23 @@ function buildIssues(args) {
4964
6216
  error_ui_visible_on_failure: {
4965
6217
  msg: "No visible error UI (color: red) found in any owned file.",
4966
6218
  fix: "Add a `<div style={{color: 'red'}}>{error}</div>` block to your CometChatNoSSR.tsx (or equivalent) so init/login failures are visible to the user instead of silently rendering nothing."
6219
+ },
6220
+ // ── React Native checks ─────────────────────────────────────────────────
6221
+ gesture_handler_line_1: {
6222
+ msg: '`import "react-native-gesture-handler"` is not line 1 of the entry file (index.js / App.tsx / app/_layout.tsx).',
6223
+ fix: 'Add `import "react-native-gesture-handler";` as the FIRST line of your entry file \u2014 above every other import. Gesture handler requires native setup that must happen before any React code runs, and some bundlers defer non-leading imports which breaks release builds silently.'
6224
+ },
6225
+ four_wrapper_chain_present: {
6226
+ msg: "One or more of the 4 required wrappers is missing: GestureHandlerRootView \u2192 SafeAreaProvider \u2192 CometChatThemeProvider \u2192 CometChatProvider.",
6227
+ fix: "Wrap your app root (App.tsx or app/_layout.tsx) with all 4 wrappers in that exact order. Omitting any of them breaks gestures, safe areas, theming, or login state \u2014 and it fails silently in dev. See cometchat-native-core \xA7 3."
6228
+ },
6229
+ hide_reply_in_thread_option: {
6230
+ msg: "A <CometChatMessageList> was found without `hideReplyInThreadOption`, and no <CometChatThreadHeader> is wired.",
6231
+ fix: 'Add `hideReplyInThreadOption={true}` to every <CometChatMessageList> in the integration \u2014 OR wire a full thread panel (CometChatThreadHeader + scoped list + composer with parentMessageId). Without the flag, tapping a message shows a "Reply in Thread" action that leads to a broken thread screen.'
6232
+ },
6233
+ pod_install_run: {
6234
+ msg: "ios/Podfile exists but ios/Podfile.lock is missing \u2014 pod install has not been run.",
6235
+ fix: "Run `cd ios && pod install && cd ..` to link the native modules. After a new package install, pod install must run before `npm run ios` or Xcode build will fail."
4967
6236
  }
4968
6237
  };
4969
6238
  for (const failed of args.verify.failed) {
@@ -5029,7 +6298,8 @@ async function doctor(args) {
5029
6298
  verifyResult = runQuickVerify(
5030
6299
  root,
5031
6300
  stateInfo.files_owned,
5032
- stateInfo.files_patched.map((p2) => p2.path)
6301
+ stateInfo.files_patched.map((p2) => p2.path),
6302
+ stateInfo.framework
5033
6303
  );
5034
6304
  }
5035
6305
  const expectedEnvVars = detected.env_prefix !== null ? [
@@ -5544,6 +6814,7 @@ function printHumanReadable12(r) {
5544
6814
  init_state();
5545
6815
  import { readFileSync as readFileSync11 } from "node:fs";
5546
6816
  import { join as join14, resolve as resolve15 } from "node:path";
6817
+ init_config();
5547
6818
  var HELP14 = `
5548
6819
  cometchat add-user-mgmt \u2014 create server-side user management endpoints
5549
6820
 
@@ -5589,13 +6860,28 @@ async function addUserMgmt(args) {
5589
6860
  return 0;
5590
6861
  }
5591
6862
  const root = projectPath14(args);
5592
- if (!hasState(root)) {
6863
+ const stateFramework = hasState(root) ? readState(root)?.framework ?? null : null;
6864
+ const cfgFramework = readConfig(root)?.framework ?? null;
6865
+ const resolvedFramework = stateFramework ?? cfgFramework;
6866
+ if (!resolvedFramework) {
5593
6867
  return outputResult10(args, {
5594
6868
  status: "no-integration",
5595
6869
  framework: "",
5596
6870
  files_created: [],
5597
6871
  next_steps: [],
5598
- error: "API_ERROR: ERR_NO_INTEGRATION No integration found. Run `cometchat apply` first to create the base integration, then run `cometchat add-user-mgmt`."
6872
+ error: "API_ERROR: ERR_NO_INTEGRATION No integration found. Run `cometchat apply` (web) or `/cometchat` (React Native) first, then re-run `cometchat add-user-mgmt`."
6873
+ }, 1);
6874
+ }
6875
+ if (resolvedFramework === "expo" || resolvedFramework === "react-native") {
6876
+ return addUserMgmtRn(args, root, resolvedFramework);
6877
+ }
6878
+ if (!hasState(root)) {
6879
+ return outputResult10(args, {
6880
+ status: "no-integration",
6881
+ framework: resolvedFramework,
6882
+ files_created: [],
6883
+ next_steps: [],
6884
+ error: "API_ERROR: ERR_NO_STATE Web add-user-mgmt requires state.json. Run `cometchat apply` first (or `cometchat state record` if your integration was hand-written)."
5599
6885
  }, 1);
5600
6886
  }
5601
6887
  const state2 = readState(root);
@@ -5742,27 +7028,126 @@ function printHumanReadable13(r) {
5742
7028
  lines.push("");
5743
7029
  console.log(lines.join("\n"));
5744
7030
  }
7031
+ function addUserMgmtRn(args, root, framework) {
7032
+ const backendFlag = args.flags.backend;
7033
+ const outDirFlag = args.flags["out-dir"];
7034
+ if (typeof backendFlag !== "string" || !RN_BACKENDS.includes(backendFlag)) {
7035
+ return outputResult10(args, {
7036
+ status: "error",
7037
+ framework,
7038
+ files_created: [],
7039
+ next_steps: [],
7040
+ error: `React Native add-user-mgmt requires --backend <${RN_BACKENDS.join("|")}>. Example: \`cometchat add-user-mgmt --backend express --out-dir ./server\`. Typically pair it with the same backend you chose for \`production-auth\`.`
7041
+ }, 1);
7042
+ }
7043
+ const backend = backendFlag;
7044
+ const tpl = rnUserMgmtTemplate(backend);
7045
+ const defaultOutDir = backend === "firebase-functions" ? "functions/src" : backend === "nextjs-api" ? "src/app/api/cometchat-user" : "server";
7046
+ const outDir = typeof outDirFlag === "string" ? outDirFlag : defaultOutDir;
7047
+ const filePath = join14(outDir, tpl.filename);
7048
+ let writeResult;
7049
+ try {
7050
+ writeResult = applyWrites(root, [
7051
+ { type: "create", path: filePath, content: tpl.content, owned: true }
7052
+ ]);
7053
+ } catch (err) {
7054
+ return errorOut7(args, "RN add-user-mgmt apply failed: " + (err instanceof Error ? err.message : String(err)));
7055
+ }
7056
+ if (writeResult.files_skipped.length > 0) {
7057
+ return outputResult10(args, {
7058
+ status: "already-applied",
7059
+ framework,
7060
+ files_created: [],
7061
+ next_steps: [
7062
+ `Skipped: ${filePath} already exists. Delete it to regenerate.`
7063
+ ],
7064
+ error: `API_ERROR: ERR_ALREADY_APPLIED ${filePath} already exists.`
7065
+ }, 0);
7066
+ }
7067
+ const next_steps = [
7068
+ `1. Backend deps: ${tpl.deps.length > 0 ? `\`npm install ${tpl.deps.join(" ")}\` in ${outDir.split("/")[0] ?? outDir}` : "(none \u2014 endpoint ships ready to deploy)"}.`,
7069
+ `2. Set server env vars (NO EXPO_PUBLIC_ prefix):`,
7070
+ ` COMETCHAT_APP_ID=<your-app-id>`,
7071
+ ` COMETCHAT_REGION=<us|eu|in>`,
7072
+ ` COMETCHAT_AUTH_KEY=<your-auth-key> # SERVER-ONLY`,
7073
+ `3. Start the backend: ${tpl.runCommand ?? "(deploy per platform convention)"}`,
7074
+ `4. \u26A0 Add authentication BEFORE deploying. The endpoint ships unauthenticated \u2014 anyone who hits it can create/update/delete CometChat users. Verify the caller's session / token in each handler.`,
7075
+ `5. Wire the endpoint into your RN app's flows:`,
7076
+ ` - On signup (after your auth provider creates a user): POST /cometchat-user with { uid, name, avatar? }`,
7077
+ ` - On profile update: PATCH /cometchat-user/<uid> with the changed fields`,
7078
+ ` - On account delete: DELETE /cometchat-user/<uid>`,
7079
+ ` See cometchat-native-production skill \xA7 6 for Firebase Auth / Supabase / Clerk / Auth0 recipes.`
7080
+ ];
7081
+ appendAuditEntry(root, {
7082
+ command: "add-user-mgmt",
7083
+ summary: `Scaffolded ${tpl.label} at ${filePath}`,
7084
+ inputs: { framework, backend, "out-dir": outDir },
7085
+ decisions: {
7086
+ backend: `${tpl.label}. Ships unauthenticated \u2014 caller must add session checks.`,
7087
+ file: `Wrote ${filePath} (owned). Deps: ${tpl.deps.length > 0 ? tpl.deps.join(", ") : "none"}.`
7088
+ },
7089
+ files_patched: [filePath],
7090
+ next_actions: next_steps
7091
+ });
7092
+ return outputResult10(args, {
7093
+ status: "applied",
7094
+ framework,
7095
+ files_created: [filePath],
7096
+ next_steps
7097
+ }, 0);
7098
+ }
5745
7099
 
5746
7100
  // src/commands/apply-feature.ts
5747
7101
  init_state();
5748
7102
  import { readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "node:fs";
5749
7103
  import { join as join15, resolve as resolve16 } from "node:path";
5750
7104
  var HELP15 = `
5751
- cometchat apply-feature \u2014 apply a component-swap feature on an integration
7105
+ cometchat apply-feature \u2014 apply a feature on top of an existing integration
5752
7106
 
5753
7107
  Usage:
5754
- cometchat apply-feature <feature-id> [--path <p>] [--json]
7108
+ cometchat apply-feature <feature-id> [--app-id <id>] [--openai-key <k>] [--path <p>] [--json]
5755
7109
 
5756
7110
  What this does:
5757
- Applies a component-swap feature (e.g. rich-text-formatting) on top of
5758
- an existing integration. Walks state.files_owned, runs a word-boundary
5759
- regex replace of swap_from \u2192 swap_to, updates checksums, and records the
5760
- feature id in state.applied_features. Idempotent.
7111
+ Dispatches by feature type from the catalog:
7112
+ extension Calls the dashboard API to flip the toggle. No code change.
7113
+ ai-feature Same, plus prompts/accepts an OpenAI key once per app.
7114
+ component-swap Walks files_owned and runs a word-boundary identifier
7115
+ swap (e.g. CometChatMessageComposer \u2192
7116
+ CometChatCompactMessageComposer). Updates checksums.
7117
+ default No-op \u2014 feature is already enabled by the UI Kit.
7118
+ dashboard-only Prints the dashboard path. Cannot automate (third-party
7119
+ keys, multi-field config).
7120
+ package-install Prints the npm install command. Cannot automate.
7121
+
7122
+ In stateful mode (default for web/RN where \`cometchat apply\` was run):
7123
+ - Reads .cometchat/state.json
7124
+ - Resolves the App ID from the project's env file
7125
+ - Records applied features in state.json for idempotency
7126
+
7127
+ In stateless mode (\`--app-id\` passed; for native iOS/Android/Flutter/Angular
7128
+ projects that don't go through \`cometchat apply\`):
7129
+ - Skips state.json
7130
+ - Uses the explicit --app-id value
7131
+ - Skips applied_features recording (server-side toggles are idempotent)
7132
+ - component-swap is unavailable (no files_owned to walk)
5761
7133
 
5762
7134
  Flags:
5763
- --path <p> Project root (defaults to cwd).
5764
- --json Machine-readable JSON output.
5765
- --help, -h Show this help.
7135
+ --app-id <id> Stateless mode \u2014 toggle the feature on this app via the
7136
+ dashboard API without requiring \`cometchat apply\` to
7137
+ have been run first. Required for native cohorts
7138
+ (iOS / Android / Flutter / Angular).
7139
+ --openai-key <k> Pre-fill the OpenAI API key for ai-feature applies. If
7140
+ omitted, the command checks the app's existing AI
7141
+ settings; if no key is set, returns an error asking
7142
+ the user to pass --openai-key.
7143
+ --path <p> Project root (defaults to cwd).
7144
+ --json Machine-readable JSON output.
7145
+ --help, -h Show this help.
7146
+
7147
+ Auth:
7148
+ extension and ai-feature applies require a dashboard bearer token.
7149
+ Run \`cometchat auth login\` first if you haven't already. The token is
7150
+ stored in the OS keychain (or ~/.cometchat-cli/credentials.json fallback).
5766
7151
  `;
5767
7152
  function isJsonMode16(args) {
5768
7153
  return args.flags.json === true || args.flags.json === "true";
@@ -5778,6 +7163,24 @@ function catalogPath2() {
5778
7163
  function loadCatalog2() {
5779
7164
  return JSON.parse(readFileSync12(catalogPath2(), "utf8"));
5780
7165
  }
7166
+ function resolveAppId2(root, state2) {
7167
+ const envFileName = envFileNameFor(state2.framework);
7168
+ if (!envFileName) return null;
7169
+ const envPath = join15(root, envFileName);
7170
+ let content;
7171
+ try {
7172
+ content = readFileSync12(envPath, "utf8");
7173
+ } catch {
7174
+ return null;
7175
+ }
7176
+ const pairs = parseEnvPairs(content);
7177
+ const prefix = state2.env_var_prefix ?? "";
7178
+ const prefixed = pairs.get(`${prefix}COMETCHAT_APP_ID`);
7179
+ const bare = pairs.get("COMETCHAT_APP_ID");
7180
+ const value = (prefixed ?? bare ?? "").trim();
7181
+ if (value.length === 0 || isPlaceholder(value)) return null;
7182
+ return value;
7183
+ }
5781
7184
  function swapIdentifier(content, from, to) {
5782
7185
  const escaped = from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5783
7186
  const re = new RegExp(`\\b${escaped}\\b`, "g");
@@ -5806,15 +7209,22 @@ async function applyFeature(args) {
5806
7209
  result.error = "Usage: cometchat apply-feature <feature-id>";
5807
7210
  return outputResult11(args, result, 1);
5808
7211
  }
5809
- if (!hasState(root)) {
7212
+ const appIdFlag = args.flags["app-id"];
7213
+ const explicitAppId = typeof appIdFlag === "string" ? appIdFlag.trim() : "";
7214
+ let state2 = null;
7215
+ let stateless = false;
7216
+ if (explicitAppId.length > 0) {
7217
+ stateless = true;
7218
+ } else if (hasState(root)) {
7219
+ state2 = readState(root);
7220
+ if (!state2) {
7221
+ result.status = "error";
7222
+ result.error = "state.json exists but could not be parsed (schema mismatch?)";
7223
+ return outputResult11(args, result, 1);
7224
+ }
7225
+ } else {
5810
7226
  result.status = "no-integration";
5811
- result.error = "No CometChat integration found. Run `cometchat apply` first.";
5812
- return outputResult11(args, result, 1);
5813
- }
5814
- const state2 = readState(root);
5815
- if (!state2) {
5816
- result.status = "error";
5817
- result.error = "state.json exists but could not be parsed (schema mismatch?)";
7227
+ result.error = "No CometChat integration found. Run `cometchat apply` first, or pass `--app-id <id>` for stateless API toggles (native cohorts: iOS/Android/Flutter/Angular).";
5818
7228
  return outputResult11(args, result, 1);
5819
7229
  }
5820
7230
  let catalog;
@@ -5831,26 +7241,192 @@ async function applyFeature(args) {
5831
7241
  result.error = `Feature "${featureId}" not in the catalog. Run \`cometchat features list\` to see available features.`;
5832
7242
  return outputResult11(args, result, 1);
5833
7243
  }
5834
- if (feature.type !== "component-swap") {
5835
- result.status = "unsupported-feature-type";
5836
- result.error = `Feature "${featureId}" is type "${feature.type}", not "component-swap". Only component-swap features can be applied via this command.`;
7244
+ result.feature_type = feature.type;
7245
+ if (state2?.applied_features?.includes(featureId)) {
7246
+ result.status = "already-applied";
7247
+ result.next_steps = [`${feature.name} is already applied to this integration.`];
7248
+ return outputResult11(args, result, 0);
7249
+ }
7250
+ switch (feature.type) {
7251
+ case "default":
7252
+ return applyDefault(args, result, feature);
7253
+ case "component-swap":
7254
+ if (stateless || !state2) {
7255
+ result.status = "state-required";
7256
+ result.error = `Feature "${feature.id}" is type component-swap and requires \`cometchat apply\` to have run first (so we know which files to swap). \`--app-id\` mode does not support component-swap features.`;
7257
+ return outputResult11(args, result, 1);
7258
+ }
7259
+ return applyComponentSwap(args, result, root, state2, feature);
7260
+ case "extension":
7261
+ return applyExtensionToggle(args, result, root, state2, explicitAppId, feature);
7262
+ case "ai-feature":
7263
+ return applyAiFeatureToggle(args, result, root, state2, explicitAppId, feature);
7264
+ case "dashboard-only":
7265
+ return applyDashboardOnly(args, result, feature);
7266
+ case "package-install":
7267
+ return applyPackageInstall(args, result, feature);
7268
+ default: {
7269
+ result.status = "unsupported-feature-type";
7270
+ result.error = `Feature "${featureId}" has unknown type "${feature.type}".`;
7271
+ return outputResult11(args, result, 1);
7272
+ }
7273
+ }
7274
+ }
7275
+ function applyDefault(args, result, feature) {
7276
+ result.status = "already-applied";
7277
+ result.next_steps = [
7278
+ `${feature.name} is enabled by default in the UI Kit. No action needed.`,
7279
+ ...feature.docs_topic ? [`Docs: https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}`] : []
7280
+ ];
7281
+ return outputResult11(args, result, 0);
7282
+ }
7283
+ async function applyExtensionToggle(args, result, root, state2, explicitAppId, feature) {
7284
+ const host = getApiHost();
7285
+ const creds = await loadToken(host);
7286
+ if (!creds) {
7287
+ result.status = "auth-required";
7288
+ result.error = "No dashboard bearer token found.";
5837
7289
  result.next_steps = [
5838
- feature.type === "default" ? `${feature.name} is already enabled \u2014 no action needed.` : feature.type === "dashboard-toggle" ? `${feature.name} is enabled in the CometChat dashboard, not via this command. Run \`cometchat features info ${featureId}\`.` : feature.type === "package-install" ? `${feature.name} requires \`npm install\` of an extra package. Run \`cometchat features info ${featureId}\`.` : `Run \`cometchat features info ${featureId}\` for instructions.`
7290
+ "Run `cometchat auth login` to authorize the CLI, then re-run this command."
5839
7291
  ];
5840
7292
  return outputResult11(args, result, 1);
5841
7293
  }
5842
- if (!feature.swap_from || !feature.swap_to) {
7294
+ const appId = explicitAppId || (state2 ? resolveAppId2(root, state2) : null);
7295
+ if (!appId) {
5843
7296
  result.status = "error";
5844
- result.error = `Feature "${featureId}" is missing swap_from / swap_to in the catalog.`;
7297
+ result.error = state2 ? "Could not read COMETCHAT_APP_ID from the project's env file." : "App ID is required. Pass `--app-id <id>`.";
7298
+ result.next_steps = state2 ? [`Confirm \`${state2.env_var_prefix ?? ""}COMETCHAT_APP_ID\` is set in your env file.`] : ["Run `cometchat apply-feature <id> --app-id <your-app-id>`."];
5845
7299
  return outputResult11(args, result, 1);
5846
7300
  }
5847
- if (state2.applied_features?.includes(featureId)) {
5848
- result.status = "already-applied";
7301
+ try {
7302
+ await toggleExtension(host, creds.token, appId, feature.id, "enable");
7303
+ } catch (err) {
7304
+ return handleApiError(args, result, err, feature);
7305
+ }
7306
+ if (state2) {
7307
+ state2.applied_features = [...state2.applied_features ?? [], feature.id];
7308
+ writeState(root, state2);
7309
+ appendAuditEntry(root, {
7310
+ command: `apply-feature ${feature.id}`,
7311
+ summary: `Enabled extension "${feature.name}" via dashboard API on app ${appId}`,
7312
+ inputs: { feature: feature.id, appId },
7313
+ decisions: {
7314
+ api_endpoint: `POST /apps/${appId}/extensions { enabled: ["${feature.id}"] }`,
7315
+ auto_wired: feature.auto_wired_in_uikit ? "yes" : "no"
7316
+ },
7317
+ files_modified: [],
7318
+ next_actions: [`Refresh the chat in the browser. ${feature.name} is now active.`]
7319
+ });
7320
+ }
7321
+ result.next_steps = [
7322
+ `Enabled ${feature.name} on app ${appId}.`,
7323
+ 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.",
7324
+ ...feature.docs_topic ? [`Docs: https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}`] : []
7325
+ ];
7326
+ return outputResult11(args, result, 0);
7327
+ }
7328
+ async function applyAiFeatureToggle(args, result, root, state2, explicitAppId, feature) {
7329
+ if (!feature.ai_key) {
7330
+ result.status = "error";
7331
+ result.error = `Feature "${feature.id}" is type ai-feature but has no ai_key in the catalog.`;
7332
+ return outputResult11(args, result, 1);
7333
+ }
7334
+ const host = getApiHost();
7335
+ const creds = await loadToken(host);
7336
+ if (!creds) {
7337
+ result.status = "auth-required";
7338
+ result.error = "No dashboard bearer token found.";
5849
7339
  result.next_steps = [
5850
- `${feature.name} is already applied to this integration.`,
5851
- `Files using ${feature.swap_to}: scan with \`grep -r "${feature.swap_to}" .\`.`
7340
+ "Run `cometchat auth login` to authorize the CLI, then re-run this command."
5852
7341
  ];
5853
- return outputResult11(args, result, 0);
7342
+ return outputResult11(args, result, 1);
7343
+ }
7344
+ const appId = explicitAppId || (state2 ? resolveAppId2(root, state2) : null);
7345
+ if (!appId) {
7346
+ result.status = "error";
7347
+ result.error = state2 ? "Could not read COMETCHAT_APP_ID from the project's env file." : "App ID is required. Pass `--app-id <id>`.";
7348
+ result.next_steps = state2 ? [`Confirm \`${state2.env_var_prefix ?? ""}COMETCHAT_APP_ID\` is set in your env file.`] : ["Run `cometchat apply-feature <id> --app-id <your-app-id> --openai-key <sk-\u2026>`."];
7349
+ return outputResult11(args, result, 1);
7350
+ }
7351
+ const openaiFlag = args.flags["openai-key"];
7352
+ const explicitKey = typeof openaiFlag === "string" ? openaiFlag.trim() : "";
7353
+ let needsKeyWrite = false;
7354
+ if (explicitKey.length > 0) {
7355
+ needsKeyWrite = true;
7356
+ } else {
7357
+ let existing;
7358
+ try {
7359
+ existing = await getAiSettings(host, creds.token, appId);
7360
+ } catch (err) {
7361
+ return handleApiError(args, result, err, feature);
7362
+ }
7363
+ const currentKey = (existing?.openAIKey ?? "").trim();
7364
+ if (currentKey.length === 0) {
7365
+ result.status = "openai-key-required";
7366
+ result.error = `${feature.name} requires an OpenAI API key. None is set on app ${appId}.`;
7367
+ result.next_steps = [
7368
+ "Pass `--openai-key <sk-\u2026>` to set the key and enable the feature in one step.",
7369
+ "Get a key from https://platform.openai.com/api-keys"
7370
+ ];
7371
+ return outputResult11(args, result, 1);
7372
+ }
7373
+ }
7374
+ try {
7375
+ if (needsKeyWrite) {
7376
+ await updateAiSettings(host, creds.token, appId, { openAIKey: explicitKey });
7377
+ }
7378
+ await toggleAiFeature(host, creds.token, appId, feature.ai_key, "enable");
7379
+ } catch (err) {
7380
+ return handleApiError(args, result, err, feature);
7381
+ }
7382
+ if (state2) {
7383
+ state2.applied_features = [...state2.applied_features ?? [], feature.id];
7384
+ writeState(root, state2);
7385
+ appendAuditEntry(root, {
7386
+ command: `apply-feature ${feature.id}`,
7387
+ summary: `Enabled AI feature "${feature.name}" via dashboard API on app ${appId}`,
7388
+ inputs: { feature: feature.id, appId, openai_key_written: needsKeyWrite },
7389
+ decisions: {
7390
+ settings_endpoint: needsKeyWrite ? `PUT /apps/${appId}/ai/settings { openAIKey: "(redacted)" }` : "skipped (key already set)",
7391
+ toggle_endpoint: `POST /apps/${appId}/features/ai.${feature.ai_key}/enabled`
7392
+ },
7393
+ files_modified: [],
7394
+ next_actions: [`Refresh the chat. AI ${feature.name} is now active.`]
7395
+ });
7396
+ }
7397
+ result.next_steps = [
7398
+ `Enabled ${feature.name} on app ${appId}.`,
7399
+ needsKeyWrite ? "OpenAI key stored on the app's AI settings." : "OpenAI key was already set.",
7400
+ "Refresh the chat in the browser to see the feature in action.",
7401
+ ...feature.docs_topic ? [`Docs: https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}`] : []
7402
+ ];
7403
+ return outputResult11(args, result, 0);
7404
+ }
7405
+ function applyDashboardOnly(args, result, feature) {
7406
+ result.status = "manual-action-required";
7407
+ result.error = `${feature.name} requires config beyond a boolean toggle (third-party API key or multi-field setup).`;
7408
+ result.next_steps = [
7409
+ 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.`,
7410
+ ...feature.docs_topic ? [`Docs: https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}`] : []
7411
+ ];
7412
+ return outputResult11(args, result, 1);
7413
+ }
7414
+ function applyPackageInstall(args, result, feature) {
7415
+ result.status = "manual-action-required";
7416
+ result.error = `${feature.name} requires installing an additional package.`;
7417
+ const pkg = feature.package ?? "the calls SDK";
7418
+ result.next_steps = [
7419
+ `Run \`npm install ${pkg}\` and restart your dev server.`,
7420
+ "The UI Kit's initiateAfterLogin() detects the SDK and wires up call buttons automatically.",
7421
+ ...feature.docs_topic ? [`Docs: https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}`] : []
7422
+ ];
7423
+ return outputResult11(args, result, 1);
7424
+ }
7425
+ function applyComponentSwap(args, result, root, state2, feature) {
7426
+ if (!feature.swap_from || !feature.swap_to) {
7427
+ result.status = "error";
7428
+ result.error = `Feature "${feature.id}" is missing swap_from / swap_to in the catalog.`;
7429
+ return outputResult11(args, result, 1);
5854
7430
  }
5855
7431
  const newChecksums = { ...state2.checksums };
5856
7432
  const modified = [];
@@ -5883,7 +7459,7 @@ async function applyFeature(args) {
5883
7459
  const nextState = {
5884
7460
  ...state2,
5885
7461
  checksums: newChecksums,
5886
- applied_features: [...state2.applied_features ?? [], featureId]
7462
+ applied_features: [...state2.applied_features ?? [], feature.id]
5887
7463
  };
5888
7464
  writeState(root, nextState);
5889
7465
  result.files_modified = modified;
@@ -5894,9 +7470,9 @@ async function applyFeature(args) {
5894
7470
  ...feature.docs_topic ? [`Docs: https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}`] : []
5895
7471
  ];
5896
7472
  appendAuditEntry(root, {
5897
- command: `apply-feature ${featureId}`,
7473
+ command: `apply-feature ${feature.id}`,
5898
7474
  summary: `Applied component-swap feature "${feature.name}" \u2014 replaced ${feature.swap_from} with ${feature.swap_to} in ${modified.length} owned file(s)`,
5899
- inputs: { feature: featureId },
7475
+ inputs: { feature: feature.id },
5900
7476
  decisions: {
5901
7477
  swap_target: `${feature.swap_from} \u2192 ${feature.swap_to} (canonical drop-in variant from @cometchat/chat-uikit-react)`,
5902
7478
  file_selection: `walked state.files_owned and ran a word-boundary regex replace; only files that contained the canonical token were touched`,
@@ -5907,6 +7483,24 @@ async function applyFeature(args) {
5907
7483
  });
5908
7484
  return outputResult11(args, result, 0);
5909
7485
  }
7486
+ function handleApiError(args, result, err, feature) {
7487
+ const msg = err instanceof Error ? err.message : String(err);
7488
+ if (msg === "AUTH_FAILED") {
7489
+ result.status = "auth-required";
7490
+ result.error = "Bearer token rejected (expired or invalid).";
7491
+ result.next_steps = ["Run `cometchat auth login` to refresh the token, then re-run this command."];
7492
+ return outputResult11(args, result, 1);
7493
+ }
7494
+ if (msg === "NETWORK") {
7495
+ result.status = "error";
7496
+ result.error = "Network error reaching the dashboard API.";
7497
+ result.next_steps = ["Check your connection and re-run."];
7498
+ return outputResult11(args, result, 1);
7499
+ }
7500
+ result.status = "error";
7501
+ result.error = `Failed to enable ${feature.name}: ${msg}`;
7502
+ return outputResult11(args, result, 1);
7503
+ }
5910
7504
  function outputResult11(args, result, exitCode) {
5911
7505
  if (isJsonMode16(args)) {
5912
7506
  console.log(JSON.stringify(enrichResultWithError(result), null, 2));
@@ -5920,9 +7514,11 @@ function printHumanReadable14(r) {
5920
7514
  lines.push("");
5921
7515
  if (r.status === "applied") {
5922
7516
  lines.push(` \u2713 Applied feature: ${r.feature}`);
5923
- lines.push("");
5924
- lines.push(" Files modified:");
5925
- for (const f of r.files_modified) lines.push(` ${f}`);
7517
+ if (r.files_modified.length > 0) {
7518
+ lines.push("");
7519
+ lines.push(" Files modified:");
7520
+ for (const f of r.files_modified) lines.push(` ${f}`);
7521
+ }
5926
7522
  } else if (r.status === "already-applied") {
5927
7523
  lines.push(` \u25E6 ${r.feature} is already applied \u2014 nothing to do.`);
5928
7524
  } else {
@@ -6495,6 +8091,7 @@ Usage:
6495
8091
  cometchat auth login [--token <bearer>] [--json]
6496
8092
  cometchat auth signup [--json]
6497
8093
  cometchat auth status [--json]
8094
+ cometchat auth me [--json]
6498
8095
  cometchat auth logout [--json]
6499
8096
 
6500
8097
  \`auth login\` and \`auth signup\` open your browser at the CometChat
@@ -6533,6 +8130,8 @@ async function auth(args) {
6533
8130
  return authLogin(args);
6534
8131
  case "status":
6535
8132
  return authStatus(args);
8133
+ case "me":
8134
+ return authMe(args);
6536
8135
  case "logout":
6537
8136
  return authLogout(args);
6538
8137
  case "signup":
@@ -6691,6 +8290,81 @@ async function authStatus(args) {
6691
8290
  console.log("");
6692
8291
  return 0;
6693
8292
  }
8293
+ async function authMe(args) {
8294
+ const json = isJsonMode18(args);
8295
+ const apiHost = getApiHost();
8296
+ const creds = await loadToken(apiHost);
8297
+ if (!creds) {
8298
+ return printError(json, {
8299
+ status: "logged-out",
8300
+ error: "No bearer token found. Run `cometchat auth login` first."
8301
+ });
8302
+ }
8303
+ let user;
8304
+ try {
8305
+ user = await getCurrentUserWithLastApp(apiHost, creds.token);
8306
+ } catch (err) {
8307
+ const msg = err instanceof Error ? err.message : String(err);
8308
+ if (msg === "AUTH_FAILED") {
8309
+ return printError(json, {
8310
+ status: "auth-required",
8311
+ error: "Bearer token rejected (expired or invalid).",
8312
+ next_step: "Run `cometchat auth login` to refresh the token."
8313
+ });
8314
+ }
8315
+ return printError(json, { status: "error", error: msg });
8316
+ }
8317
+ const meta = user.meta ?? {};
8318
+ const lastApp = user.last_app ?? null;
8319
+ const payload = {
8320
+ status: "logged-in",
8321
+ email: user.email ?? creds.email ?? null,
8322
+ name: user.name ?? null,
8323
+ role: typeof meta.role === "string" ? meta.role : null,
8324
+ other_role: typeof meta.otherRole === "string" && meta.otherRole.length > 0 ? meta.otherRole : null,
8325
+ intent: typeof meta.intent === "string" ? meta.intent : null,
8326
+ last_app: lastApp ? {
8327
+ id: lastApp.id,
8328
+ name: lastApp.name,
8329
+ region: lastApp.region,
8330
+ industry: lastApp.industry ?? null,
8331
+ technology: lastApp.technology ?? null,
8332
+ product: lastApp.product ?? null
8333
+ } : null
8334
+ };
8335
+ if (json) {
8336
+ console.log(JSON.stringify(payload, null, 2));
8337
+ return 0;
8338
+ }
8339
+ console.log("");
8340
+ console.log(` \u2713 ${payload.email ?? "(no email on profile)"}`);
8341
+ if (payload.name) console.log(` Name: ${payload.name}`);
8342
+ console.log(` Role: ${payload.role ?? "(not set)"}${payload.other_role ? ` (${payload.other_role})` : ""}`);
8343
+ console.log(` Intent: ${payload.intent ?? "(not set)"}`);
8344
+ if (payload.last_app) {
8345
+ const la = payload.last_app;
8346
+ console.log(` Last app: ${la.name} (${la.region})`);
8347
+ if (la.industry || la.technology || la.product) {
8348
+ const bits = [
8349
+ la.industry && `industry=${la.industry}`,
8350
+ la.technology && `tech=${la.technology}`,
8351
+ la.product && `product=${la.product}`
8352
+ ].filter(Boolean);
8353
+ console.log(` ${bits.join(", ")}`);
8354
+ }
8355
+ }
8356
+ console.log("");
8357
+ return 0;
8358
+ }
8359
+ function printError(json, payload) {
8360
+ if (json) {
8361
+ console.log(JSON.stringify(payload, null, 2));
8362
+ } else {
8363
+ console.error(`\u2717 ${payload.error ?? payload.status}`);
8364
+ if (payload.next_step) console.error(` ${payload.next_step}`);
8365
+ }
8366
+ return 1;
8367
+ }
6694
8368
  async function authLogout(args) {
6695
8369
  const json = isJsonMode18(args);
6696
8370
  const verbose = args.flags.verbose === true || args.flags.verbose === "true";
@@ -6711,6 +8385,8 @@ function stripNoise(result) {
6711
8385
  return result;
6712
8386
  }
6713
8387
  const { apiHost: _h, backend: _b, ...rest } = result;
8388
+ void _h;
8389
+ void _b;
6714
8390
  return rest;
6715
8391
  }
6716
8392
  function emit(json, result) {
@@ -6876,7 +8552,7 @@ async function provisionSetup(args) {
6876
8552
  return 1;
6877
8553
  }
6878
8554
  if (!frameworkFlag) {
6879
- const msg = "Missing --framework. One of: reactjs, nextjs, react-router, astro.";
8555
+ const msg = "Missing --framework. One of: reactjs, nextjs, react-router, astro, expo, react-native, angular, android, flutter, ios.";
6880
8556
  if (json) return emitError(true, msg);
6881
8557
  console.error(msg);
6882
8558
  return 1;
@@ -6965,7 +8641,31 @@ var ENV_PREFIX_BY_FRAMEWORK = {
6965
8641
  reactjs: "VITE_",
6966
8642
  nextjs: "NEXT_PUBLIC_",
6967
8643
  "react-router": "VITE_",
6968
- astro: "PUBLIC_"
8644
+ astro: "PUBLIC_",
8645
+ expo: "EXPO_PUBLIC_",
8646
+ "react-native": "",
8647
+ // bare RN uses react-native-dotenv — no prefix
8648
+ angular: "",
8649
+ // Angular reads from src/environments/environment.ts; CLI writes
8650
+ // a no-prefix .env as a credentials handoff, and the skill
8651
+ // migrates the values into environment.ts during integration
8652
+ android: "",
8653
+ // Android reads from local.properties → BuildConfig at compile
8654
+ // time. CLI writes a no-prefix .env handoff; the skill teaches
8655
+ // the user to mirror values into local.properties + add
8656
+ // buildConfigField entries to app/build.gradle.
8657
+ flutter: "",
8658
+ // Flutter has no runtime .env. Credentials come from
8659
+ // --dart-define flags or a generated config Dart file. CLI
8660
+ // writes a no-prefix .env handoff; the skill teaches the
8661
+ // integration agent how to wire creds (typically a
8662
+ // const-class file + runner script with --dart-define).
8663
+ ios: ""
8664
+ // iOS has no runtime .env. Credentials live in a
8665
+ // Secrets.swift const file (gitignored) or get injected
8666
+ // via xcconfig build settings. CLI writes a no-prefix
8667
+ // .env handoff for the agent; the skill teaches the
8668
+ // Secrets.swift / xcconfig migration.
6969
8669
  };
6970
8670
  async function provisionList(args) {
6971
8671
  const json = isJsonMode19(args);
@@ -8437,6 +10137,14 @@ function projectPath18(args) {
8437
10137
  if (typeof fromFlag === "string") return resolve21(fromFlag);
8438
10138
  return resolve21(process.cwd());
8439
10139
  }
10140
+ var STATE_ENV_PREFIX_BY_FRAMEWORK = {
10141
+ reactjs: "VITE_",
10142
+ nextjs: "NEXT_PUBLIC_",
10143
+ "react-router": "VITE_",
10144
+ astro: "PUBLIC_",
10145
+ expo: "EXPO_PUBLIC_",
10146
+ "react-native": ""
10147
+ };
8440
10148
  async function state(args) {
8441
10149
  if (args.flags.help || args.flags.h) {
8442
10150
  console.log(HELP21);
@@ -8474,7 +10182,7 @@ async function stateRecord(args) {
8474
10182
  return 1;
8475
10183
  }
8476
10184
  if (!envPrefix) {
8477
- envPrefix = framework === "nextjs" ? "NEXT_PUBLIC_" : framework === "astro" ? "PUBLIC_" : "VITE_";
10185
+ envPrefix = STATE_ENV_PREFIX_BY_FRAMEWORK[framework] ?? "VITE_";
8478
10186
  }
8479
10187
  const experienceStr = asString5(args.flags.experience) ?? "0";
8480
10188
  const experience = parseInt(experienceStr, 10);