@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/LICENSE +21 -0
- package/dist/index.js +1814 -106
- package/dist/registry/v6/features/catalog.json +42 -26
- package/package.json +4 -4
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
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
|
-
--
|
|
3356
|
-
|
|
3357
|
-
--
|
|
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
|
-
|
|
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
|
|
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
|
|
4347
|
-
|
|
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
|
|
4375
|
-
const res = await request(
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
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 (!
|
|
4388
|
-
return data
|
|
5513
|
+
if (!data || typeof data !== "object") return null;
|
|
5514
|
+
return data;
|
|
4389
5515
|
}
|
|
4390
|
-
async function
|
|
4391
|
-
const
|
|
4392
|
-
|
|
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(
|
|
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
|
|
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 "
|
|
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()
|
|
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.
|
|
4498
|
-
` 2.
|
|
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
|
-
`
|
|
4504
|
-
`
|
|
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.
|
|
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 "
|
|
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
|
|
4529
|
-
`
|
|
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 === "
|
|
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 !== "
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
|
|
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`
|
|
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
|
|
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
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
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
|
-
--
|
|
5764
|
-
|
|
5765
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5835
|
-
|
|
5836
|
-
result.
|
|
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
|
-
|
|
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
|
-
|
|
7294
|
+
const appId = explicitAppId || (state2 ? resolveAppId2(root, state2) : null);
|
|
7295
|
+
if (!appId) {
|
|
5843
7296
|
result.status = "error";
|
|
5844
|
-
result.error =
|
|
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
|
-
|
|
5848
|
-
|
|
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
|
-
|
|
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,
|
|
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 ?? [],
|
|
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 ${
|
|
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:
|
|
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
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
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
|
|
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);
|