@cometchat/skills-cli 2.1.0 → 2.2.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 +1103 -34
- package/dist/registry/v6/features/catalog.json +9 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -309,7 +309,10 @@ var EMPTY = {
|
|
|
309
309
|
router: null,
|
|
310
310
|
bundler: null,
|
|
311
311
|
ssr_strategy: null,
|
|
312
|
-
env_prefix: null
|
|
312
|
+
env_prefix: null,
|
|
313
|
+
expo_mode: null,
|
|
314
|
+
expo_version: null,
|
|
315
|
+
react_native_version: null
|
|
313
316
|
};
|
|
314
317
|
function allDeps(pkg) {
|
|
315
318
|
return {
|
|
@@ -320,6 +323,50 @@ function allDeps(pkg) {
|
|
|
320
323
|
function hasViteConfig(root) {
|
|
321
324
|
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
325
|
}
|
|
326
|
+
function detectExpo(root, deps, pkg) {
|
|
327
|
+
if (!deps["expo"]) return null;
|
|
328
|
+
const expoVersion = extractVersion(deps["expo"]);
|
|
329
|
+
const rnVersion = deps["react-native"] ? extractVersion(deps["react-native"]) : null;
|
|
330
|
+
const hasExpoRouter = !!deps["expo-router"];
|
|
331
|
+
const mainIsExpoRouter = pkg.main === "expo-router/entry" || pkg.main === "expo-router/entry.js";
|
|
332
|
+
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"));
|
|
333
|
+
const usingExpoRouter = hasExpoRouter && (mainIsExpoRouter || hasAppLayout);
|
|
334
|
+
const expoMode = usingExpoRouter ? "expo-router" : "managed";
|
|
335
|
+
return {
|
|
336
|
+
framework: "expo",
|
|
337
|
+
framework_version: expoVersion,
|
|
338
|
+
router: usingExpoRouter ? "expo-router" : "react-navigation",
|
|
339
|
+
bundler: "metro",
|
|
340
|
+
ssr_strategy: "native-no-ssr",
|
|
341
|
+
env_prefix: "EXPO_PUBLIC_",
|
|
342
|
+
expo_mode: expoMode,
|
|
343
|
+
expo_version: expoVersion,
|
|
344
|
+
react_native_version: rnVersion
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function detectBareReactNative(root, deps) {
|
|
348
|
+
if (!deps["react-native"]) return null;
|
|
349
|
+
if (deps["expo"]) return null;
|
|
350
|
+
const hasNativeIos = pathExists(p(root, "ios")) && (pathExists(p(root, "ios/Podfile")) || pathExists(p(root, "ios/Podfile.properties.json")));
|
|
351
|
+
const hasNativeAndroid = pathExists(p(root, "android")) && pathExists(p(root, "android/build.gradle"));
|
|
352
|
+
const hasRnConfig = pathExists(p(root, "react-native.config.js")) || pathExists(p(root, "react-native.config.ts"));
|
|
353
|
+
if (!hasNativeIos && !hasNativeAndroid && !hasRnConfig) {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
const rnVersion = extractVersion(deps["react-native"]);
|
|
357
|
+
return {
|
|
358
|
+
framework: "react-native",
|
|
359
|
+
framework_version: rnVersion,
|
|
360
|
+
router: "react-navigation",
|
|
361
|
+
bundler: "metro",
|
|
362
|
+
ssr_strategy: "native-no-ssr",
|
|
363
|
+
env_prefix: null,
|
|
364
|
+
// Bare RN uses react-native-dotenv or a config module — no prefix
|
|
365
|
+
expo_mode: null,
|
|
366
|
+
expo_version: null,
|
|
367
|
+
react_native_version: rnVersion
|
|
368
|
+
};
|
|
369
|
+
}
|
|
323
370
|
function detectNextjs(root, deps) {
|
|
324
371
|
if (!deps["next"]) return null;
|
|
325
372
|
const version = extractVersion(deps["next"]);
|
|
@@ -421,7 +468,7 @@ function detectFramework(root) {
|
|
|
421
468
|
const pkg = readJsonOrNull(p(root, "package.json"));
|
|
422
469
|
if (!pkg) return EMPTY;
|
|
423
470
|
const deps = allDeps(pkg);
|
|
424
|
-
return detectNextjs(root, deps) ?? detectAstro(root, deps) ?? detectReactRouter(root, deps) ?? detectReactjs(root, deps) ?? EMPTY;
|
|
471
|
+
return detectExpo(root, deps, pkg) ?? detectBareReactNative(root, deps) ?? detectNextjs(root, deps) ?? detectAstro(root, deps) ?? detectReactRouter(root, deps) ?? detectReactjs(root, deps) ?? EMPTY;
|
|
425
472
|
}
|
|
426
473
|
|
|
427
474
|
// src/detectors/package-manager.ts
|
|
@@ -633,10 +680,49 @@ function computeCompatibility(fw) {
|
|
|
633
680
|
return {
|
|
634
681
|
supported: false,
|
|
635
682
|
warnings: [
|
|
636
|
-
"Could not detect a supported React framework. CometChat skills
|
|
683
|
+
"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
684
|
]
|
|
638
685
|
};
|
|
639
686
|
}
|
|
687
|
+
if (fw.framework === "expo") {
|
|
688
|
+
if (fw.expo_version) {
|
|
689
|
+
const expoMajor = parseInt(fw.expo_version.split(".")[0] ?? "0", 10);
|
|
690
|
+
if (expoMajor < 49) {
|
|
691
|
+
return {
|
|
692
|
+
supported: false,
|
|
693
|
+
warnings: [
|
|
694
|
+
`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.`
|
|
695
|
+
]
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (fw.react_native_version) {
|
|
700
|
+
const rnMinor = parseFloat(fw.react_native_version.split(".").slice(0, 2).join("."));
|
|
701
|
+
if (!isNaN(rnMinor) && rnMinor < 0.72) {
|
|
702
|
+
warnings.push(
|
|
703
|
+
`React Native ${fw.react_native_version} is below the recommended baseline (0.72+). Some UI Kit v5 features may not work.`
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (fw.framework === "react-native") {
|
|
709
|
+
if (fw.react_native_version) {
|
|
710
|
+
const rnMinor = parseFloat(fw.react_native_version.split(".").slice(0, 2).join("."));
|
|
711
|
+
if (!isNaN(rnMinor) && rnMinor < 0.7) {
|
|
712
|
+
return {
|
|
713
|
+
supported: false,
|
|
714
|
+
warnings: [
|
|
715
|
+
`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.`
|
|
716
|
+
]
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
if (rnMinor < 0.72) {
|
|
720
|
+
warnings.push(
|
|
721
|
+
`React Native ${fw.react_native_version} is below the recommended baseline (0.72+). Upgrade is recommended.`
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
640
726
|
if (fw.framework === "nextjs") {
|
|
641
727
|
if (fw.router === null) {
|
|
642
728
|
warnings.push(
|
|
@@ -750,6 +836,9 @@ async function runDetectors(root) {
|
|
|
750
836
|
env_prefix: fw.env_prefix,
|
|
751
837
|
uses_jsx: fw.uses_jsx,
|
|
752
838
|
react_router_mode: fw.react_router_mode,
|
|
839
|
+
expo_mode: fw.expo_mode,
|
|
840
|
+
expo_version: fw.expo_version,
|
|
841
|
+
react_native_version: fw.react_native_version,
|
|
753
842
|
package_manager,
|
|
754
843
|
credentials,
|
|
755
844
|
existing_integration,
|
|
@@ -826,10 +915,17 @@ function printHumanReadable(r) {
|
|
|
826
915
|
lines.push("");
|
|
827
916
|
lines.push(` Project: ${r.project_root}`);
|
|
828
917
|
lines.push(` Framework: ${r.framework ?? "unknown"}${r.framework_version ? ` (${r.framework_version})` : ""}`);
|
|
918
|
+
if (r.expo_mode && r.expo_mode !== null) lines.push(` Expo mode: ${r.expo_mode}`);
|
|
919
|
+
if (r.react_native_version && r.framework === "expo") {
|
|
920
|
+
lines.push(` React Native: ${r.react_native_version}`);
|
|
921
|
+
}
|
|
829
922
|
if (r.router !== null) lines.push(` Router: ${r.router}`);
|
|
830
923
|
if (r.bundler !== null) lines.push(` Bundler: ${r.bundler}`);
|
|
831
924
|
if (r.ssr_strategy !== null) lines.push(` SSR strategy: ${r.ssr_strategy}`);
|
|
832
925
|
if (r.env_prefix !== null) lines.push(` Env var prefix: ${r.env_prefix}`);
|
|
926
|
+
if (r.framework === "react-native" && r.env_prefix === null) {
|
|
927
|
+
lines.push(` Env var prefix: (none \u2014 bare RN uses react-native-dotenv or a config module)`);
|
|
928
|
+
}
|
|
833
929
|
if (r.package_manager !== null) lines.push(` Package manager: ${r.package_manager}`);
|
|
834
930
|
if (r.uses_jsx === true) {
|
|
835
931
|
lines.push(` Source language: JavaScript (.jsx) \u2014 \u26A0 apply will refuse, the v6 templates are TypeScript-only`);
|
|
@@ -2890,6 +2986,73 @@ function checkInitBeforeLogin(root, ownedAndPatchedFiles) {
|
|
|
2890
2986
|
}
|
|
2891
2987
|
return { status: "skip", reason: "no file calling CometChatUIKit.init found" };
|
|
2892
2988
|
}
|
|
2989
|
+
function checkGestureHandlerLine1(root) {
|
|
2990
|
+
const entryCandidates = ["index.js", "index.ts", "App.tsx", "App.jsx", "app/_layout.tsx", "app/_layout.js"];
|
|
2991
|
+
for (const e of entryCandidates) {
|
|
2992
|
+
if (!pathExists(p(root, e))) continue;
|
|
2993
|
+
const content = readFileOrNull(p(root, e));
|
|
2994
|
+
if (!content) continue;
|
|
2995
|
+
const firstLine = content.split(/\r?\n/)[0]?.trim() ?? "";
|
|
2996
|
+
if (/^import\s+['"]react-native-gesture-handler['"]/.test(firstLine)) {
|
|
2997
|
+
return { status: "pass" };
|
|
2998
|
+
}
|
|
2999
|
+
return {
|
|
3000
|
+
status: "fail",
|
|
3001
|
+
reason: `${e}: \`import "react-native-gesture-handler"\` is not the first line`
|
|
3002
|
+
};
|
|
3003
|
+
}
|
|
3004
|
+
return { status: "skip", reason: "no entry file found" };
|
|
3005
|
+
}
|
|
3006
|
+
function checkFourWrapperChain(root, ownedAndPatchedFiles) {
|
|
3007
|
+
const wrappers = ["GestureHandlerRootView", "SafeAreaProvider", "CometChatThemeProvider", "CometChatProvider"];
|
|
3008
|
+
const hits = /* @__PURE__ */ new Set();
|
|
3009
|
+
for (const file of ownedAndPatchedFiles) {
|
|
3010
|
+
const content = readFileOrNull(p(root, file));
|
|
3011
|
+
if (!content) continue;
|
|
3012
|
+
for (const w of wrappers) {
|
|
3013
|
+
if (content.includes(w)) hits.add(w);
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
const missing = wrappers.filter((w) => !hits.has(w));
|
|
3017
|
+
if (missing.length === 0) return { status: "pass" };
|
|
3018
|
+
return {
|
|
3019
|
+
status: "fail",
|
|
3020
|
+
reason: `missing wrapper(s): ${missing.join(", ")}`
|
|
3021
|
+
};
|
|
3022
|
+
}
|
|
3023
|
+
function checkHideReplyInThread(root, ownedAndPatchedFiles, ownedFiles) {
|
|
3024
|
+
for (const file of ownedAndPatchedFiles) {
|
|
3025
|
+
const content = readFileOrNull(p(root, file));
|
|
3026
|
+
if (content && content.includes("CometChatThreadHeader")) {
|
|
3027
|
+
return { status: "pass" };
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
for (const file of ownedFiles) {
|
|
3031
|
+
const content = readFileOrNull(p(root, file));
|
|
3032
|
+
if (!content) continue;
|
|
3033
|
+
const lists = (content.match(/<\s*CometChatMessageList\b/g) ?? []).length;
|
|
3034
|
+
const flagged = (content.match(/hideReplyInThreadOption/g) ?? []).length;
|
|
3035
|
+
if (lists > flagged) {
|
|
3036
|
+
return {
|
|
3037
|
+
status: "fail",
|
|
3038
|
+
reason: `${file}: ${lists - flagged} MessageList without hideReplyInThreadOption`
|
|
3039
|
+
};
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
return { status: "pass" };
|
|
3043
|
+
}
|
|
3044
|
+
function checkPodInstall(root) {
|
|
3045
|
+
if (!pathExists(p(root, "ios/Podfile"))) {
|
|
3046
|
+
return { status: "skip", reason: "no ios/Podfile (Expo managed or no iOS target)" };
|
|
3047
|
+
}
|
|
3048
|
+
if (pathExists(p(root, "ios/Podfile.lock"))) {
|
|
3049
|
+
return { status: "pass" };
|
|
3050
|
+
}
|
|
3051
|
+
return {
|
|
3052
|
+
status: "fail",
|
|
3053
|
+
reason: "ios/Podfile.lock missing \u2014 run `cd ios && pod install && cd ..`"
|
|
3054
|
+
};
|
|
3055
|
+
}
|
|
2893
3056
|
function checkErrorUiVisible(root, ownedFiles) {
|
|
2894
3057
|
for (const file of ownedFiles) {
|
|
2895
3058
|
const content = readFileOrNull(p(root, file));
|
|
@@ -2927,7 +3090,15 @@ async function verify(args) {
|
|
|
2927
3090
|
...state2.files_owned,
|
|
2928
3091
|
...state2.files_patched.map((p2) => p2.path)
|
|
2929
3092
|
];
|
|
2930
|
-
const
|
|
3093
|
+
const isRn2 = state2.framework === "expo" || state2.framework === "react-native";
|
|
3094
|
+
const checks = isRn2 ? {
|
|
3095
|
+
gesture_handler_line_1: checkGestureHandlerLine1(root),
|
|
3096
|
+
four_wrapper_chain_present: checkFourWrapperChain(root, ownedAndPatched),
|
|
3097
|
+
hide_reply_in_thread_option: checkHideReplyInThread(root, ownedAndPatched, state2.files_owned),
|
|
3098
|
+
no_auth_key_in_source: checkNoAuthKeyInSource(root, state2.files_owned),
|
|
3099
|
+
init_before_login: checkInitBeforeLogin(root, ownedAndPatched),
|
|
3100
|
+
...state2.framework === "react-native" ? { pod_install_run: checkPodInstall(root) } : {}
|
|
3101
|
+
} : {
|
|
2931
3102
|
css_variables_imported_once: checkCssVariablesImport(root, ownedAndPatched),
|
|
2932
3103
|
init_before_login: checkInitBeforeLogin(root, ownedAndPatched),
|
|
2933
3104
|
render_gated_on_login_resolve: checkRenderGatedOnLogin(root, ownedAndPatched),
|
|
@@ -3337,24 +3508,485 @@ function printHumanReadable7(r) {
|
|
|
3337
3508
|
init_state();
|
|
3338
3509
|
import { readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
|
|
3339
3510
|
import { join as join10, resolve as resolve9 } from "node:path";
|
|
3511
|
+
init_config();
|
|
3512
|
+
|
|
3513
|
+
// src/utils/rn-production-templates.ts
|
|
3514
|
+
var HEADER = `/**
|
|
3515
|
+
* /cometchat-token \u2014 server-side endpoint that mints CometChat auth tokens.
|
|
3516
|
+
*
|
|
3517
|
+
* This is the production-mode replacement for client-side login({ uid }) +
|
|
3518
|
+
* Auth Key. The Auth Key NEVER leaves the server. The RN app calls this
|
|
3519
|
+
* endpoint to get a short-lived auth token, then passes it to
|
|
3520
|
+
* CometChatUIKit.login({ authToken }).
|
|
3521
|
+
*
|
|
3522
|
+
* Generated by \`cometchat production-auth\`. Customize freely:
|
|
3523
|
+
* - Replace the ?uid= query param with real auth: read the session token,
|
|
3524
|
+
* extract the user ID, map it to a CometChat UID.
|
|
3525
|
+
* - Add rate limiting if exposed publicly.
|
|
3526
|
+
* - Cache tokens per UID for short windows to reduce CometChat API load.
|
|
3527
|
+
*
|
|
3528
|
+
* Security: this file MUST NOT use EXPO_PUBLIC_ prefixes for the Auth Key.
|
|
3529
|
+
* The whole point is to keep the Auth Key out of the client bundle.
|
|
3530
|
+
*/`;
|
|
3531
|
+
var EXPRESS_TEMPLATE = `${HEADER}
|
|
3532
|
+
import express from "express";
|
|
3533
|
+
|
|
3534
|
+
const app = express();
|
|
3535
|
+
app.use(express.json());
|
|
3536
|
+
|
|
3537
|
+
const COMETCHAT_APP_ID = process.env.COMETCHAT_APP_ID!;
|
|
3538
|
+
const COMETCHAT_REGION = process.env.COMETCHAT_REGION!;
|
|
3539
|
+
const COMETCHAT_AUTH_KEY = process.env.COMETCHAT_AUTH_KEY!;
|
|
3540
|
+
|
|
3541
|
+
app.get("/cometchat-token", async (req, res) => {
|
|
3542
|
+
// \u26A0 Replace this with your real auth: extract the session token, verify it,
|
|
3543
|
+
// and map your user record to a CometChat UID. Do not trust the query param.
|
|
3544
|
+
const uid = typeof req.query.uid === "string" ? req.query.uid : "cometchat-uid-1";
|
|
3545
|
+
|
|
3546
|
+
if (!COMETCHAT_APP_ID || !COMETCHAT_REGION || !COMETCHAT_AUTH_KEY) {
|
|
3547
|
+
return res.status(500).json({ error: "CometChat credentials not configured on the server" });
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
try {
|
|
3551
|
+
const upstream = await fetch(
|
|
3552
|
+
\`https://\${COMETCHAT_APP_ID}.api-\${COMETCHAT_REGION}.cometchat.io/v3/users/\${encodeURIComponent(uid)}/auth_tokens\`,
|
|
3553
|
+
{
|
|
3554
|
+
method: "POST",
|
|
3555
|
+
headers: {
|
|
3556
|
+
"Content-Type": "application/json",
|
|
3557
|
+
appid: COMETCHAT_APP_ID,
|
|
3558
|
+
apikey: COMETCHAT_AUTH_KEY,
|
|
3559
|
+
},
|
|
3560
|
+
},
|
|
3561
|
+
);
|
|
3562
|
+
if (!upstream.ok) {
|
|
3563
|
+
const text = await upstream.text();
|
|
3564
|
+
return res.status(502).json({ error: \`CometChat token mint failed (\${upstream.status})\`, detail: text });
|
|
3565
|
+
}
|
|
3566
|
+
const body = (await upstream.json()) as { data?: { authToken?: string } };
|
|
3567
|
+
const authToken = body.data?.authToken;
|
|
3568
|
+
if (!authToken) {
|
|
3569
|
+
return res.status(502).json({ error: "CometChat API returned no authToken" });
|
|
3570
|
+
}
|
|
3571
|
+
return res.json({ authToken });
|
|
3572
|
+
} catch (err) {
|
|
3573
|
+
return res.status(500).json({ error: "Upstream CometChat request failed", detail: String(err) });
|
|
3574
|
+
}
|
|
3575
|
+
});
|
|
3576
|
+
|
|
3577
|
+
const PORT = Number(process.env.PORT ?? 8787);
|
|
3578
|
+
app.listen(PORT, () => console.log(\`cometchat-token endpoint listening on :\${PORT}\`));
|
|
3579
|
+
`;
|
|
3580
|
+
var HONO_TEMPLATE = `${HEADER}
|
|
3581
|
+
import { Hono } from "hono";
|
|
3582
|
+
|
|
3583
|
+
const app = new Hono();
|
|
3584
|
+
|
|
3585
|
+
app.get("/cometchat-token", async (c) => {
|
|
3586
|
+
const COMETCHAT_APP_ID = process.env.COMETCHAT_APP_ID!;
|
|
3587
|
+
const COMETCHAT_REGION = process.env.COMETCHAT_REGION!;
|
|
3588
|
+
const COMETCHAT_AUTH_KEY = process.env.COMETCHAT_AUTH_KEY!;
|
|
3589
|
+
|
|
3590
|
+
// \u26A0 Replace this with your real auth: extract the session token, verify it,
|
|
3591
|
+
// and map your user record to a CometChat UID. Do not trust the query param.
|
|
3592
|
+
const uid = c.req.query("uid") ?? "cometchat-uid-1";
|
|
3593
|
+
|
|
3594
|
+
if (!COMETCHAT_APP_ID || !COMETCHAT_REGION || !COMETCHAT_AUTH_KEY) {
|
|
3595
|
+
return c.json({ error: "CometChat credentials not configured on the server" }, 500);
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
try {
|
|
3599
|
+
const upstream = await fetch(
|
|
3600
|
+
\`https://\${COMETCHAT_APP_ID}.api-\${COMETCHAT_REGION}.cometchat.io/v3/users/\${encodeURIComponent(uid)}/auth_tokens\`,
|
|
3601
|
+
{
|
|
3602
|
+
method: "POST",
|
|
3603
|
+
headers: {
|
|
3604
|
+
"Content-Type": "application/json",
|
|
3605
|
+
appid: COMETCHAT_APP_ID,
|
|
3606
|
+
apikey: COMETCHAT_AUTH_KEY,
|
|
3607
|
+
},
|
|
3608
|
+
},
|
|
3609
|
+
);
|
|
3610
|
+
if (!upstream.ok) {
|
|
3611
|
+
return c.json({ error: \`CometChat token mint failed (\${upstream.status})\` }, 502);
|
|
3612
|
+
}
|
|
3613
|
+
const body = (await upstream.json()) as { data?: { authToken?: string } };
|
|
3614
|
+
const authToken = body.data?.authToken;
|
|
3615
|
+
if (!authToken) return c.json({ error: "CometChat API returned no authToken" }, 502);
|
|
3616
|
+
return c.json({ authToken });
|
|
3617
|
+
} catch (err) {
|
|
3618
|
+
return c.json({ error: "Upstream CometChat request failed", detail: String(err) }, 500);
|
|
3619
|
+
}
|
|
3620
|
+
});
|
|
3621
|
+
|
|
3622
|
+
export default app;
|
|
3623
|
+
`;
|
|
3624
|
+
var FIREBASE_TEMPLATE = `${HEADER}
|
|
3625
|
+
import { onRequest } from "firebase-functions/v2/https";
|
|
3626
|
+
import { defineSecret } from "firebase-functions/params";
|
|
3627
|
+
|
|
3628
|
+
const COMETCHAT_APP_ID = defineSecret("COMETCHAT_APP_ID");
|
|
3629
|
+
const COMETCHAT_REGION = defineSecret("COMETCHAT_REGION");
|
|
3630
|
+
const COMETCHAT_AUTH_KEY = defineSecret("COMETCHAT_AUTH_KEY");
|
|
3631
|
+
|
|
3632
|
+
export const cometchatToken = onRequest(
|
|
3633
|
+
{ secrets: [COMETCHAT_APP_ID, COMETCHAT_REGION, COMETCHAT_AUTH_KEY] },
|
|
3634
|
+
async (req, res) => {
|
|
3635
|
+
// \u26A0 Replace this with your real auth: verify Firebase ID token, map the
|
|
3636
|
+
// Firebase uid to a CometChat UID. Do not trust the query param.
|
|
3637
|
+
const uid = typeof req.query.uid === "string" ? req.query.uid : "cometchat-uid-1";
|
|
3638
|
+
|
|
3639
|
+
try {
|
|
3640
|
+
const upstream = await fetch(
|
|
3641
|
+
\`https://\${COMETCHAT_APP_ID.value()}.api-\${COMETCHAT_REGION.value()}.cometchat.io/v3/users/\${encodeURIComponent(uid)}/auth_tokens\`,
|
|
3642
|
+
{
|
|
3643
|
+
method: "POST",
|
|
3644
|
+
headers: {
|
|
3645
|
+
"Content-Type": "application/json",
|
|
3646
|
+
appid: COMETCHAT_APP_ID.value(),
|
|
3647
|
+
apikey: COMETCHAT_AUTH_KEY.value(),
|
|
3648
|
+
},
|
|
3649
|
+
},
|
|
3650
|
+
);
|
|
3651
|
+
if (!upstream.ok) {
|
|
3652
|
+
res.status(502).json({ error: \`CometChat token mint failed (\${upstream.status})\` });
|
|
3653
|
+
return;
|
|
3654
|
+
}
|
|
3655
|
+
const body = (await upstream.json()) as { data?: { authToken?: string } };
|
|
3656
|
+
const authToken = body.data?.authToken;
|
|
3657
|
+
if (!authToken) {
|
|
3658
|
+
res.status(502).json({ error: "CometChat API returned no authToken" });
|
|
3659
|
+
return;
|
|
3660
|
+
}
|
|
3661
|
+
res.json({ authToken });
|
|
3662
|
+
} catch (err) {
|
|
3663
|
+
res.status(500).json({ error: "Upstream CometChat request failed", detail: String(err) });
|
|
3664
|
+
}
|
|
3665
|
+
},
|
|
3666
|
+
);
|
|
3667
|
+
`;
|
|
3668
|
+
var NEXTJS_API_TEMPLATE = `${HEADER}
|
|
3669
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
3670
|
+
|
|
3671
|
+
const COMETCHAT_APP_ID = process.env.COMETCHAT_APP_ID!;
|
|
3672
|
+
const COMETCHAT_REGION = process.env.COMETCHAT_REGION!;
|
|
3673
|
+
const COMETCHAT_AUTH_KEY = process.env.COMETCHAT_AUTH_KEY!;
|
|
3674
|
+
|
|
3675
|
+
export async function GET(request: NextRequest) {
|
|
3676
|
+
// \u26A0 Replace this with your real auth: read the session, extract the user ID,
|
|
3677
|
+
// then map it to a CometChat UID. Do not trust the query param in production.
|
|
3678
|
+
const uid = request.nextUrl.searchParams.get("uid") ?? "cometchat-uid-1";
|
|
3679
|
+
|
|
3680
|
+
if (!COMETCHAT_APP_ID || !COMETCHAT_REGION || !COMETCHAT_AUTH_KEY) {
|
|
3681
|
+
return NextResponse.json(
|
|
3682
|
+
{ error: "CometChat credentials not configured on the server" },
|
|
3683
|
+
{ status: 500 },
|
|
3684
|
+
);
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
try {
|
|
3688
|
+
const upstream = await fetch(
|
|
3689
|
+
\`https://\${COMETCHAT_APP_ID}.api-\${COMETCHAT_REGION}.cometchat.io/v3/users/\${encodeURIComponent(uid)}/auth_tokens\`,
|
|
3690
|
+
{
|
|
3691
|
+
method: "POST",
|
|
3692
|
+
headers: {
|
|
3693
|
+
"Content-Type": "application/json",
|
|
3694
|
+
appid: COMETCHAT_APP_ID,
|
|
3695
|
+
apikey: COMETCHAT_AUTH_KEY,
|
|
3696
|
+
},
|
|
3697
|
+
},
|
|
3698
|
+
);
|
|
3699
|
+
if (!upstream.ok) {
|
|
3700
|
+
const text = await upstream.text();
|
|
3701
|
+
return NextResponse.json({ error: \`CometChat token mint failed (\${upstream.status})\`, detail: text }, { status: 502 });
|
|
3702
|
+
}
|
|
3703
|
+
const body = (await upstream.json()) as { data?: { authToken?: string } };
|
|
3704
|
+
const authToken = body.data?.authToken;
|
|
3705
|
+
if (!authToken) return NextResponse.json({ error: "CometChat API returned no authToken" }, { status: 502 });
|
|
3706
|
+
return NextResponse.json({ authToken });
|
|
3707
|
+
} catch (err) {
|
|
3708
|
+
return NextResponse.json({ error: "Upstream CometChat request failed", detail: String(err) }, { status: 500 });
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
`;
|
|
3712
|
+
var USER_MGMT_HEADER = `/**
|
|
3713
|
+
* /cometchat-user \u2014 server-side user CRUD endpoint.
|
|
3714
|
+
*
|
|
3715
|
+
* Proxies to the CometChat REST API to keep your AUTH_KEY out of the client.
|
|
3716
|
+
* Wire this into your app's signup / profile-update / account-deletion flows
|
|
3717
|
+
* so every app user has a matching CometChat user.
|
|
3718
|
+
*
|
|
3719
|
+
* Generated by \`cometchat add-user-mgmt\`. Ships UNAUTHENTICATED \u2014 you MUST
|
|
3720
|
+
* add session checks (verify the caller's auth token / cookie) before
|
|
3721
|
+
* deploying. Anyone hitting this endpoint can create/update/delete arbitrary
|
|
3722
|
+
* CometChat users.
|
|
3723
|
+
*/`;
|
|
3724
|
+
var EXPRESS_USER_TEMPLATE = `${USER_MGMT_HEADER}
|
|
3725
|
+
import express from "express";
|
|
3726
|
+
|
|
3727
|
+
const app = express();
|
|
3728
|
+
app.use(express.json());
|
|
3729
|
+
|
|
3730
|
+
const APP_ID = process.env.COMETCHAT_APP_ID!;
|
|
3731
|
+
const REGION = process.env.COMETCHAT_REGION!;
|
|
3732
|
+
const AUTH_KEY = process.env.COMETCHAT_AUTH_KEY!;
|
|
3733
|
+
const BASE = \`https://\${APP_ID}.api-\${REGION}.cometchat.io/v3/users\`;
|
|
3734
|
+
|
|
3735
|
+
async function cometchat(method: string, path: string, body?: unknown): Promise<Response> {
|
|
3736
|
+
return fetch(\`\${BASE}\${path}\`, {
|
|
3737
|
+
method,
|
|
3738
|
+
headers: { "Content-Type": "application/json", appid: APP_ID, apikey: AUTH_KEY },
|
|
3739
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
3740
|
+
});
|
|
3741
|
+
}
|
|
3742
|
+
|
|
3743
|
+
app.post("/cometchat-user", async (req, res) => {
|
|
3744
|
+
// \u26A0 Add auth check here \u2014 verify the caller owns the uid they're creating.
|
|
3745
|
+
const { uid, name, avatar, metadata } = req.body ?? {};
|
|
3746
|
+
if (!uid || !name) return res.status(400).json({ error: "uid + name required" });
|
|
3747
|
+
const r = await cometchat("POST", "", { uid, name, avatar, metadata });
|
|
3748
|
+
return res.status(r.ok ? 201 : r.status).json(await r.json());
|
|
3749
|
+
});
|
|
3750
|
+
|
|
3751
|
+
app.patch("/cometchat-user/:uid", async (req, res) => {
|
|
3752
|
+
const r = await cometchat("PUT", \`/\${encodeURIComponent(req.params.uid)}\`, req.body);
|
|
3753
|
+
return res.status(r.ok ? 200 : r.status).json(await r.json());
|
|
3754
|
+
});
|
|
3755
|
+
|
|
3756
|
+
app.delete("/cometchat-user/:uid", async (req, res) => {
|
|
3757
|
+
const r = await cometchat("DELETE", \`/\${encodeURIComponent(req.params.uid)}?permanent=true\`);
|
|
3758
|
+
return res.status(r.ok ? 200 : r.status).json(r.ok ? { deleted: true } : await r.json());
|
|
3759
|
+
});
|
|
3760
|
+
|
|
3761
|
+
const PORT = Number(process.env.PORT ?? 8787);
|
|
3762
|
+
app.listen(PORT, () => console.log(\`cometchat-user endpoint listening on :\${PORT}\`));
|
|
3763
|
+
`;
|
|
3764
|
+
var HONO_USER_TEMPLATE = `${USER_MGMT_HEADER}
|
|
3765
|
+
import { Hono } from "hono";
|
|
3766
|
+
|
|
3767
|
+
const app = new Hono();
|
|
3768
|
+
|
|
3769
|
+
async function cometchat(method: string, path: string, body?: unknown): Promise<Response> {
|
|
3770
|
+
const APP_ID = process.env.COMETCHAT_APP_ID!;
|
|
3771
|
+
const REGION = process.env.COMETCHAT_REGION!;
|
|
3772
|
+
const AUTH_KEY = process.env.COMETCHAT_AUTH_KEY!;
|
|
3773
|
+
return fetch(\`https://\${APP_ID}.api-\${REGION}.cometchat.io/v3/users\${path}\`, {
|
|
3774
|
+
method,
|
|
3775
|
+
headers: { "Content-Type": "application/json", appid: APP_ID, apikey: AUTH_KEY },
|
|
3776
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
3777
|
+
});
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3780
|
+
app.post("/cometchat-user", async (c) => {
|
|
3781
|
+
// \u26A0 Add auth check here \u2014 verify the caller owns the uid they're creating.
|
|
3782
|
+
const { uid, name, avatar, metadata } = (await c.req.json()) ?? {};
|
|
3783
|
+
if (!uid || !name) return c.json({ error: "uid + name required" }, 400);
|
|
3784
|
+
const r = await cometchat("POST", "", { uid, name, avatar, metadata });
|
|
3785
|
+
return c.json(await r.json(), r.ok ? 201 : (r.status as 500));
|
|
3786
|
+
});
|
|
3787
|
+
|
|
3788
|
+
app.patch("/cometchat-user/:uid", async (c) => {
|
|
3789
|
+
const r = await cometchat("PUT", \`/\${encodeURIComponent(c.req.param("uid"))}\`, await c.req.json());
|
|
3790
|
+
return c.json(await r.json(), r.ok ? 200 : (r.status as 500));
|
|
3791
|
+
});
|
|
3792
|
+
|
|
3793
|
+
app.delete("/cometchat-user/:uid", async (c) => {
|
|
3794
|
+
const r = await cometchat("DELETE", \`/\${encodeURIComponent(c.req.param("uid"))}?permanent=true\`);
|
|
3795
|
+
return c.json(r.ok ? { deleted: true } : await r.json(), r.ok ? 200 : (r.status as 500));
|
|
3796
|
+
});
|
|
3797
|
+
|
|
3798
|
+
export default app;
|
|
3799
|
+
`;
|
|
3800
|
+
var FIREBASE_USER_TEMPLATE = `${USER_MGMT_HEADER}
|
|
3801
|
+
import { onRequest } from "firebase-functions/v2/https";
|
|
3802
|
+
import { defineSecret } from "firebase-functions/params";
|
|
3803
|
+
|
|
3804
|
+
const COMETCHAT_APP_ID = defineSecret("COMETCHAT_APP_ID");
|
|
3805
|
+
const COMETCHAT_REGION = defineSecret("COMETCHAT_REGION");
|
|
3806
|
+
const COMETCHAT_AUTH_KEY = defineSecret("COMETCHAT_AUTH_KEY");
|
|
3807
|
+
|
|
3808
|
+
async function cometchat(method: string, path: string, body?: unknown): Promise<Response> {
|
|
3809
|
+
return fetch(\`https://\${COMETCHAT_APP_ID.value()}.api-\${COMETCHAT_REGION.value()}.cometchat.io/v3/users\${path}\`, {
|
|
3810
|
+
method,
|
|
3811
|
+
headers: {
|
|
3812
|
+
"Content-Type": "application/json",
|
|
3813
|
+
appid: COMETCHAT_APP_ID.value(),
|
|
3814
|
+
apikey: COMETCHAT_AUTH_KEY.value(),
|
|
3815
|
+
},
|
|
3816
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
3817
|
+
});
|
|
3818
|
+
}
|
|
3819
|
+
|
|
3820
|
+
export const cometchatUser = onRequest(
|
|
3821
|
+
{ secrets: [COMETCHAT_APP_ID, COMETCHAT_REGION, COMETCHAT_AUTH_KEY] },
|
|
3822
|
+
async (req, res) => {
|
|
3823
|
+
// \u26A0 Add auth check: verify Firebase ID token, authorize the caller.
|
|
3824
|
+
const method = req.method;
|
|
3825
|
+
const segs = (req.path ?? "").split("/").filter(Boolean); // ["cometchat-user"] or ["cometchat-user", "<uid>"]
|
|
3826
|
+
const uid = segs.length >= 2 ? segs[1] : null;
|
|
3827
|
+
|
|
3828
|
+
if (method === "POST" && !uid) {
|
|
3829
|
+
const { uid: newUid, name, avatar, metadata } = (req.body ?? {}) as Record<string, unknown>;
|
|
3830
|
+
if (!newUid || !name) { res.status(400).json({ error: "uid + name required" }); return; }
|
|
3831
|
+
const r = await cometchat("POST", "", { uid: newUid, name, avatar, metadata });
|
|
3832
|
+
res.status(r.ok ? 201 : r.status).json(await r.json()); return;
|
|
3833
|
+
}
|
|
3834
|
+
if (method === "PATCH" && uid) {
|
|
3835
|
+
const r = await cometchat("PUT", \`/\${encodeURIComponent(uid)}\`, req.body);
|
|
3836
|
+
res.status(r.ok ? 200 : r.status).json(await r.json()); return;
|
|
3837
|
+
}
|
|
3838
|
+
if (method === "DELETE" && uid) {
|
|
3839
|
+
const r = await cometchat("DELETE", \`/\${encodeURIComponent(uid)}?permanent=true\`);
|
|
3840
|
+
res.status(r.ok ? 200 : r.status).json(r.ok ? { deleted: true } : await r.json()); return;
|
|
3841
|
+
}
|
|
3842
|
+
res.status(405).json({ error: "method/path not allowed" });
|
|
3843
|
+
},
|
|
3844
|
+
);
|
|
3845
|
+
`;
|
|
3846
|
+
var NEXTJS_USER_TEMPLATE = `${USER_MGMT_HEADER}
|
|
3847
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
3848
|
+
|
|
3849
|
+
const APP_ID = process.env.COMETCHAT_APP_ID!;
|
|
3850
|
+
const REGION = process.env.COMETCHAT_REGION!;
|
|
3851
|
+
const AUTH_KEY = process.env.COMETCHAT_AUTH_KEY!;
|
|
3852
|
+
const BASE = \`https://\${APP_ID}.api-\${REGION}.cometchat.io/v3/users\`;
|
|
3853
|
+
|
|
3854
|
+
async function cometchat(method: string, path: string, body?: unknown): Promise<Response> {
|
|
3855
|
+
return fetch(\`\${BASE}\${path}\`, {
|
|
3856
|
+
method,
|
|
3857
|
+
headers: { "Content-Type": "application/json", appid: APP_ID, apikey: AUTH_KEY },
|
|
3858
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
3859
|
+
});
|
|
3860
|
+
}
|
|
3861
|
+
|
|
3862
|
+
// POST /api/cometchat-user \u2014 create user. Wire into your signup flow.
|
|
3863
|
+
export async function POST(request: NextRequest) {
|
|
3864
|
+
// \u26A0 Add auth check here \u2014 verify the caller's session.
|
|
3865
|
+
const { uid, name, avatar, metadata } = await request.json();
|
|
3866
|
+
if (!uid || !name) return NextResponse.json({ error: "uid + name required" }, { status: 400 });
|
|
3867
|
+
const r = await cometchat("POST", "", { uid, name, avatar, metadata });
|
|
3868
|
+
return NextResponse.json(await r.json(), { status: r.ok ? 201 : r.status });
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3871
|
+
// PATCH /api/cometchat-user?uid=<id> \u2014 update user.
|
|
3872
|
+
export async function PATCH(request: NextRequest) {
|
|
3873
|
+
const uid = request.nextUrl.searchParams.get("uid");
|
|
3874
|
+
if (!uid) return NextResponse.json({ error: "uid query param required" }, { status: 400 });
|
|
3875
|
+
const r = await cometchat("PUT", \`/\${encodeURIComponent(uid)}\`, await request.json());
|
|
3876
|
+
return NextResponse.json(await r.json(), { status: r.ok ? 200 : r.status });
|
|
3877
|
+
}
|
|
3878
|
+
|
|
3879
|
+
// DELETE /api/cometchat-user?uid=<id> \u2014 permanent user delete.
|
|
3880
|
+
export async function DELETE(request: NextRequest) {
|
|
3881
|
+
const uid = request.nextUrl.searchParams.get("uid");
|
|
3882
|
+
if (!uid) return NextResponse.json({ error: "uid query param required" }, { status: 400 });
|
|
3883
|
+
const r = await cometchat("DELETE", \`/\${encodeURIComponent(uid)}?permanent=true\`);
|
|
3884
|
+
return NextResponse.json(r.ok ? { deleted: true } : await r.json(), { status: r.ok ? 200 : r.status });
|
|
3885
|
+
}
|
|
3886
|
+
`;
|
|
3887
|
+
function rnUserMgmtTemplate(backend) {
|
|
3888
|
+
switch (backend) {
|
|
3889
|
+
case "express":
|
|
3890
|
+
return {
|
|
3891
|
+
filename: "cometchat-user.ts",
|
|
3892
|
+
content: EXPRESS_USER_TEMPLATE,
|
|
3893
|
+
deps: ["express"],
|
|
3894
|
+
runCommand: "npx tsx cometchat-user.ts",
|
|
3895
|
+
label: "Express user-mgmt endpoint"
|
|
3896
|
+
};
|
|
3897
|
+
case "hono":
|
|
3898
|
+
return {
|
|
3899
|
+
filename: "cometchat-user.ts",
|
|
3900
|
+
content: HONO_USER_TEMPLATE,
|
|
3901
|
+
deps: ["hono"],
|
|
3902
|
+
runCommand: "npx tsx cometchat-user.ts # or bun run",
|
|
3903
|
+
label: "Hono user-mgmt endpoint"
|
|
3904
|
+
};
|
|
3905
|
+
case "firebase-functions":
|
|
3906
|
+
return {
|
|
3907
|
+
filename: "cometchatUser.ts",
|
|
3908
|
+
content: FIREBASE_USER_TEMPLATE,
|
|
3909
|
+
deps: ["firebase-functions"],
|
|
3910
|
+
runCommand: "firebase deploy --only functions",
|
|
3911
|
+
label: "Firebase Functions user-mgmt endpoint"
|
|
3912
|
+
};
|
|
3913
|
+
case "nextjs-api":
|
|
3914
|
+
return {
|
|
3915
|
+
filename: "route.ts",
|
|
3916
|
+
content: NEXTJS_USER_TEMPLATE,
|
|
3917
|
+
deps: [],
|
|
3918
|
+
runCommand: null,
|
|
3919
|
+
label: "Next.js API route (drop into src/app/api/cometchat-user/route.ts)"
|
|
3920
|
+
};
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
function rnBackendTemplate(backend) {
|
|
3924
|
+
switch (backend) {
|
|
3925
|
+
case "express":
|
|
3926
|
+
return {
|
|
3927
|
+
filename: "cometchat-token.ts",
|
|
3928
|
+
content: EXPRESS_TEMPLATE,
|
|
3929
|
+
deps: ["express"],
|
|
3930
|
+
runCommand: "npx tsx cometchat-token.ts",
|
|
3931
|
+
label: "Express server"
|
|
3932
|
+
};
|
|
3933
|
+
case "hono":
|
|
3934
|
+
return {
|
|
3935
|
+
filename: "cometchat-token.ts",
|
|
3936
|
+
content: HONO_TEMPLATE,
|
|
3937
|
+
deps: ["hono"],
|
|
3938
|
+
runCommand: "npx tsx cometchat-token.ts # or bun run, depending on your runtime",
|
|
3939
|
+
label: "Hono router (works with Bun, Cloudflare Workers, Node, Deno)"
|
|
3940
|
+
};
|
|
3941
|
+
case "firebase-functions":
|
|
3942
|
+
return {
|
|
3943
|
+
filename: "cometchatToken.ts",
|
|
3944
|
+
content: FIREBASE_TEMPLATE,
|
|
3945
|
+
deps: ["firebase-functions"],
|
|
3946
|
+
runCommand: "firebase deploy --only functions",
|
|
3947
|
+
label: "Firebase Cloud Functions (v2)"
|
|
3948
|
+
};
|
|
3949
|
+
case "nextjs-api":
|
|
3950
|
+
return {
|
|
3951
|
+
filename: "route.ts",
|
|
3952
|
+
content: NEXTJS_API_TEMPLATE,
|
|
3953
|
+
deps: [],
|
|
3954
|
+
runCommand: null,
|
|
3955
|
+
label: "Next.js App Router API route (add to an existing Next app under src/app/api/cometchat-token/route.ts)"
|
|
3956
|
+
};
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
var RN_BACKENDS = ["express", "hono", "firebase-functions", "nextjs-api"];
|
|
3960
|
+
|
|
3961
|
+
// src/commands/production-auth.ts
|
|
3340
3962
|
var HELP8 = `
|
|
3341
3963
|
cometchat production-auth \u2014 upgrade dev integration to server-side tokens
|
|
3342
3964
|
|
|
3343
|
-
Usage:
|
|
3965
|
+
Usage (web \u2014 nextjs / react-router / astro):
|
|
3344
3966
|
cometchat production-auth [--path <p>] [--json]
|
|
3345
3967
|
|
|
3968
|
+
Usage (React Native \u2014 expo / react-native):
|
|
3969
|
+
cometchat production-auth --backend <express|hono|firebase-functions|nextjs-api>
|
|
3970
|
+
[--out-dir <path>] [--path <p>] [--json]
|
|
3971
|
+
|
|
3346
3972
|
What this does:
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3973
|
+
Web: replaces client-side login(UID) + setAuthKey() with a server-side
|
|
3974
|
+
token endpoint so your AUTH_KEY never reaches the browser. Creates
|
|
3975
|
+
the API route file and auto-rewrites the client login chain.
|
|
3976
|
+
RN: writes a standalone token endpoint (your choice of backend \u2014 Express,
|
|
3977
|
+
Hono, Firebase Functions, or a Next.js API route file you drop into
|
|
3978
|
+
an adjacent Next app). Does not patch the RN client \u2014 prints manual
|
|
3979
|
+
steps to update your CometChatProvider.
|
|
3353
3980
|
|
|
3354
3981
|
Flags:
|
|
3355
|
-
--
|
|
3356
|
-
|
|
3357
|
-
--
|
|
3982
|
+
--backend <name> (RN only) Backend flavor. One of: express, hono,
|
|
3983
|
+
firebase-functions, nextjs-api.
|
|
3984
|
+
--out-dir <path> (RN only) Directory to write the endpoint file to
|
|
3985
|
+
(default: ./server for express/hono/nextjs-api,
|
|
3986
|
+
./functions/src for firebase-functions).
|
|
3987
|
+
--path <p> Project root (defaults to cwd).
|
|
3988
|
+
--json Machine-readable JSON output.
|
|
3989
|
+
--help, -h Show this help.
|
|
3358
3990
|
`;
|
|
3359
3991
|
function isJsonMode9(args) {
|
|
3360
3992
|
return args.flags.json === true || args.flags.json === "true";
|
|
@@ -3425,13 +4057,29 @@ async function productionAuth(args) {
|
|
|
3425
4057
|
return 0;
|
|
3426
4058
|
}
|
|
3427
4059
|
const root = projectPath8(args);
|
|
3428
|
-
|
|
4060
|
+
const stateFramework = hasState(root) ? readState(root)?.framework ?? null : null;
|
|
4061
|
+
const cfgFramework = readConfig(root)?.framework ?? null;
|
|
4062
|
+
const resolvedFramework = stateFramework ?? cfgFramework;
|
|
4063
|
+
if (!resolvedFramework) {
|
|
3429
4064
|
const result2 = {
|
|
3430
4065
|
status: "no-integration",
|
|
3431
4066
|
framework: "",
|
|
3432
4067
|
files_created: [],
|
|
3433
4068
|
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
|
|
4069
|
+
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."
|
|
4070
|
+
};
|
|
4071
|
+
return outputResult5(args, result2, 1);
|
|
4072
|
+
}
|
|
4073
|
+
if (resolvedFramework === "expo" || resolvedFramework === "react-native") {
|
|
4074
|
+
return productionAuthRn(args, root, resolvedFramework);
|
|
4075
|
+
}
|
|
4076
|
+
if (!hasState(root)) {
|
|
4077
|
+
const result2 = {
|
|
4078
|
+
status: "no-integration",
|
|
4079
|
+
framework: resolvedFramework,
|
|
4080
|
+
files_created: [],
|
|
4081
|
+
next_steps: [],
|
|
4082
|
+
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
4083
|
};
|
|
3436
4084
|
return outputResult5(args, result2, 1);
|
|
3437
4085
|
}
|
|
@@ -3639,6 +4287,84 @@ function printHumanReadable8(r) {
|
|
|
3639
4287
|
lines.push("");
|
|
3640
4288
|
console.log(lines.join("\n"));
|
|
3641
4289
|
}
|
|
4290
|
+
function productionAuthRn(args, root, framework) {
|
|
4291
|
+
const backendFlag = args.flags.backend;
|
|
4292
|
+
const outDirFlag = args.flags["out-dir"];
|
|
4293
|
+
if (typeof backendFlag !== "string" || !RN_BACKENDS.includes(backendFlag)) {
|
|
4294
|
+
const result2 = {
|
|
4295
|
+
status: "error",
|
|
4296
|
+
framework,
|
|
4297
|
+
files_created: [],
|
|
4298
|
+
next_steps: [],
|
|
4299
|
+
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.`
|
|
4300
|
+
};
|
|
4301
|
+
return outputResult5(args, result2, 1);
|
|
4302
|
+
}
|
|
4303
|
+
const backend = backendFlag;
|
|
4304
|
+
const tpl = rnBackendTemplate(backend);
|
|
4305
|
+
const defaultOutDir = backend === "firebase-functions" ? "functions/src" : backend === "nextjs-api" ? "src/app/api/cometchat-token" : "server";
|
|
4306
|
+
const outDir = typeof outDirFlag === "string" ? outDirFlag : defaultOutDir;
|
|
4307
|
+
const filePath = join10(outDir, tpl.filename);
|
|
4308
|
+
let writeResult;
|
|
4309
|
+
try {
|
|
4310
|
+
writeResult = applyWrites(root, [
|
|
4311
|
+
{ type: "create", path: filePath, content: tpl.content, owned: true }
|
|
4312
|
+
]);
|
|
4313
|
+
} catch (err) {
|
|
4314
|
+
return errorOut3(args, "RN production-auth apply failed: " + (err instanceof Error ? err.message : String(err)));
|
|
4315
|
+
}
|
|
4316
|
+
if (writeResult.files_skipped.length > 0) {
|
|
4317
|
+
const result2 = {
|
|
4318
|
+
status: "already-applied",
|
|
4319
|
+
framework,
|
|
4320
|
+
files_created: [],
|
|
4321
|
+
next_steps: [
|
|
4322
|
+
`Skipped: ${filePath} already exists. Delete it to regenerate with a different --backend.`
|
|
4323
|
+
],
|
|
4324
|
+
error: `API_ERROR: ERR_ALREADY_APPLIED ${filePath} already exists.`
|
|
4325
|
+
};
|
|
4326
|
+
return outputResult5(args, result2, 0);
|
|
4327
|
+
}
|
|
4328
|
+
const depsStr = tpl.deps.length > 0 ? tpl.deps.join(" ") : null;
|
|
4329
|
+
const restart = framework === "expo" ? "npx expo start --clear" : "npm start -- --reset-cache";
|
|
4330
|
+
const next_steps = [
|
|
4331
|
+
`1. Backend deps: ${depsStr ? `\`npm install ${depsStr}\` in ${outDir.split("/")[0] ?? outDir}` : "(none \u2014 route file ships ready to deploy)"}.`,
|
|
4332
|
+
`2. Set server env vars (NO EXPO_PUBLIC_ prefix):`,
|
|
4333
|
+
` COMETCHAT_APP_ID=<your-app-id>`,
|
|
4334
|
+
` COMETCHAT_REGION=<us|eu|in>`,
|
|
4335
|
+
` COMETCHAT_AUTH_KEY=<your-auth-key> # SERVER-ONLY, keep out of the RN bundle`,
|
|
4336
|
+
`3. Start the backend: ${tpl.runCommand ?? "(deploy per platform convention)"}`,
|
|
4337
|
+
`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.`,
|
|
4338
|
+
`5. Client-side change in your CometChatProvider:`,
|
|
4339
|
+
` - Remove \`.setAuthKey(...)\` from UIKitSettingsBuilder (keep setAppId + setRegion).`,
|
|
4340
|
+
` - In your ensureLoggedIn() helper, replace \`login({ uid })\` with:`,
|
|
4341
|
+
` const res = await fetch(\`\${API_BASE}/cometchat-token?uid=\${uid}\`);`,
|
|
4342
|
+
` const { authToken } = await res.json();`,
|
|
4343
|
+
` await CometChatUIKit.login({ authToken });`,
|
|
4344
|
+
`6. Restart Metro with \`${restart}\` and log in. The CometChat SDK never sees your Auth Key anymore.`,
|
|
4345
|
+
` See cometchat-native-production skill for auth-provider integrations (Firebase Auth, Supabase, Clerk, Auth0) and token caching.`
|
|
4346
|
+
];
|
|
4347
|
+
appendAuditEntry(root, {
|
|
4348
|
+
command: "production-auth",
|
|
4349
|
+
summary: `Scaffolded ${tpl.label} token endpoint at ${filePath}`,
|
|
4350
|
+
inputs: { framework, backend, "out-dir": outDir },
|
|
4351
|
+
decisions: {
|
|
4352
|
+
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.`,
|
|
4353
|
+
file: `Wrote ${filePath} (owned). Deps: ${depsStr ?? "none"}.`
|
|
4354
|
+
},
|
|
4355
|
+
files_patched: [filePath],
|
|
4356
|
+
next_actions: next_steps
|
|
4357
|
+
});
|
|
4358
|
+
const result = {
|
|
4359
|
+
status: "applied",
|
|
4360
|
+
framework,
|
|
4361
|
+
files_created: [filePath],
|
|
4362
|
+
client_patch: "n/a",
|
|
4363
|
+
// RN client-side is manual — too much structural variation to auto-patch
|
|
4364
|
+
next_steps
|
|
4365
|
+
};
|
|
4366
|
+
return outputResult5(args, result, 0);
|
|
4367
|
+
}
|
|
3642
4368
|
|
|
3643
4369
|
// src/commands/apply-theme.ts
|
|
3644
4370
|
init_state();
|
|
@@ -3803,6 +4529,75 @@ function buildOverrideBlock(theme) {
|
|
|
3803
4529
|
}
|
|
3804
4530
|
return { css: lines.join("\n") + "\n", variables: vars };
|
|
3805
4531
|
}
|
|
4532
|
+
function isReactNativeFramework(framework) {
|
|
4533
|
+
return framework === "expo" || framework === "react-native";
|
|
4534
|
+
}
|
|
4535
|
+
function buildRnThemeSource(theme, useTs) {
|
|
4536
|
+
const vars = [];
|
|
4537
|
+
const lightColorLines = [];
|
|
4538
|
+
lightColorLines.push(` primary: "${theme.primaryColor}",`);
|
|
4539
|
+
vars.push("color.primary");
|
|
4540
|
+
if (theme.textColor) {
|
|
4541
|
+
lightColorLines.push(` textPrimary: "${theme.textColor}",`);
|
|
4542
|
+
vars.push("color.textPrimary");
|
|
4543
|
+
}
|
|
4544
|
+
if (theme.backgroundColor) {
|
|
4545
|
+
lightColorLines.push(` background1: "${theme.backgroundColor}",`);
|
|
4546
|
+
vars.push("color.background1");
|
|
4547
|
+
}
|
|
4548
|
+
const typographyLines = [];
|
|
4549
|
+
if (theme.fontFamily) {
|
|
4550
|
+
typographyLines.push(` body1: { fontFamily: "${theme.fontFamily}" },`);
|
|
4551
|
+
typographyLines.push(` heading1: { fontFamily: "${theme.fontFamily}" },`);
|
|
4552
|
+
vars.push("typography.body1.fontFamily");
|
|
4553
|
+
vars.push("typography.heading1.fontFamily");
|
|
4554
|
+
}
|
|
4555
|
+
const darkBlock = [];
|
|
4556
|
+
if (theme.darkMode) {
|
|
4557
|
+
darkBlock.push(" dark: {");
|
|
4558
|
+
darkBlock.push(" color: {");
|
|
4559
|
+
darkBlock.push(` primary: "${theme.primaryColor}",`);
|
|
4560
|
+
darkBlock.push(` background1: "#0A0A0A",`);
|
|
4561
|
+
darkBlock.push(` background2: "#1A1A1A",`);
|
|
4562
|
+
darkBlock.push(` background3: "#2A2A2A",`);
|
|
4563
|
+
darkBlock.push(` textPrimary: "#EDEDED",`);
|
|
4564
|
+
darkBlock.push(" },");
|
|
4565
|
+
darkBlock.push(" },");
|
|
4566
|
+
vars.push("dark.color.primary");
|
|
4567
|
+
vars.push("dark.color.background1-3");
|
|
4568
|
+
vars.push("dark.color.textPrimary");
|
|
4569
|
+
}
|
|
4570
|
+
const exportType = useTs ? ": Partial<CometChatTheme>" : "";
|
|
4571
|
+
const lines = [];
|
|
4572
|
+
lines.push("// CometChat theme \u2014 generated by `cometchat apply-theme`");
|
|
4573
|
+
lines.push("// Pass this to <CometChatThemeProvider theme={cometchatTheme}> in your app root.");
|
|
4574
|
+
lines.push("");
|
|
4575
|
+
if (useTs) {
|
|
4576
|
+
lines.push(`import type { CometChatTheme } from "@cometchat/chat-uikit-react-native";`);
|
|
4577
|
+
lines.push("");
|
|
4578
|
+
}
|
|
4579
|
+
lines.push(`export const cometchatTheme${exportType} = {`);
|
|
4580
|
+
lines.push(" light: {");
|
|
4581
|
+
lines.push(" color: {");
|
|
4582
|
+
lightColorLines.forEach((l) => lines.push(l));
|
|
4583
|
+
lines.push(" },");
|
|
4584
|
+
if (typographyLines.length > 0) {
|
|
4585
|
+
lines.push(" typography: {");
|
|
4586
|
+
typographyLines.forEach((l) => lines.push(l));
|
|
4587
|
+
lines.push(" },");
|
|
4588
|
+
}
|
|
4589
|
+
lines.push(" },");
|
|
4590
|
+
if (darkBlock.length > 0) {
|
|
4591
|
+
darkBlock.forEach((l) => lines.push(l));
|
|
4592
|
+
}
|
|
4593
|
+
lines.push("};");
|
|
4594
|
+
lines.push("");
|
|
4595
|
+
return { content: lines.join("\n"), variables: vars };
|
|
4596
|
+
}
|
|
4597
|
+
function rnThemePath(root) {
|
|
4598
|
+
const isTs = pathExists(p(root, "tsconfig.json"));
|
|
4599
|
+
return isTs ? "providers/CometChatTheme.ts" : "providers/CometChatTheme.js";
|
|
4600
|
+
}
|
|
3806
4601
|
function buildThemeOp(framework, cssBlock, root) {
|
|
3807
4602
|
const targets = {
|
|
3808
4603
|
reactjs: ["src/index.css", "src/main.css", "src/styles.css"],
|
|
@@ -3904,6 +4699,54 @@ async function applyTheme(args) {
|
|
|
3904
4699
|
`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
4700
|
);
|
|
3906
4701
|
}
|
|
4702
|
+
if (isReactNativeFramework(framework)) {
|
|
4703
|
+
const themePath = rnThemePath(root);
|
|
4704
|
+
const useTs = themePath.endsWith(".ts");
|
|
4705
|
+
const { content, variables: rnVariables } = buildRnThemeSource(theme, useTs);
|
|
4706
|
+
let writeResult2;
|
|
4707
|
+
try {
|
|
4708
|
+
writeResult2 = applyWrites(root, [
|
|
4709
|
+
{ type: "create", path: themePath, content, owned: true }
|
|
4710
|
+
]);
|
|
4711
|
+
} catch (err) {
|
|
4712
|
+
return errorOut4(args, "Theme apply failed: " + (err instanceof Error ? err.message : String(err)));
|
|
4713
|
+
}
|
|
4714
|
+
const wasSkipped2 = writeResult2.files_skipped.length > 0;
|
|
4715
|
+
const wireHint = `Wire it in: import { cometchatTheme } from "./${themePath.replace(/\.(ts|js)$/, "")}"; then <CometChatThemeProvider theme={cometchatTheme}>\u2026</CometChatThemeProvider>`;
|
|
4716
|
+
const result2 = {
|
|
4717
|
+
status: "applied",
|
|
4718
|
+
framework,
|
|
4719
|
+
file_modified: themePath,
|
|
4720
|
+
variables_applied: rnVariables,
|
|
4721
|
+
next_steps: [
|
|
4722
|
+
wasSkipped2 ? `Skipped: ${themePath} already exists. Delete it to regenerate.` : `Theme object written to ${themePath}.`,
|
|
4723
|
+
wireHint,
|
|
4724
|
+
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.",
|
|
4725
|
+
"To remove the theme, delete the file and drop the `theme` prop on CometChatThemeProvider."
|
|
4726
|
+
]
|
|
4727
|
+
};
|
|
4728
|
+
if (!wasSkipped2) {
|
|
4729
|
+
const presetFlag = getStringFlag3(args, "preset");
|
|
4730
|
+
appendAuditEntry(root, {
|
|
4731
|
+
command: "apply-theme",
|
|
4732
|
+
summary: presetFlag ? `Applied "${presetFlag}" theme preset to ${framework} integration` : `Applied custom theme overrides to ${framework} integration`,
|
|
4733
|
+
inputs: {
|
|
4734
|
+
framework,
|
|
4735
|
+
...presetFlag ? { preset: presetFlag } : {},
|
|
4736
|
+
...theme.primaryColor ? { "primary-color": theme.primaryColor } : {},
|
|
4737
|
+
...theme.darkMode ? { "dark-mode": true } : {}
|
|
4738
|
+
},
|
|
4739
|
+
decisions: {
|
|
4740
|
+
target: `${framework}: wrote theme module at ${themePath} for use with <CometChatThemeProvider theme={...}>. RN uses a JS theme object, not CSS.`,
|
|
4741
|
+
...presetFlag ? { preset: `picked the "${presetFlag}" preset bundle (primary + text + background + font + dark-mode)` } : {},
|
|
4742
|
+
variables_set: rnVariables.join(", ")
|
|
4743
|
+
},
|
|
4744
|
+
files_patched: [themePath],
|
|
4745
|
+
next_actions: result2.next_steps
|
|
4746
|
+
});
|
|
4747
|
+
}
|
|
4748
|
+
return outputResult6(args, result2, 0);
|
|
4749
|
+
}
|
|
3907
4750
|
const { css, variables } = buildOverrideBlock(theme);
|
|
3908
4751
|
const { op, targetPath, specialHint } = buildThemeOp(framework, css, root);
|
|
3909
4752
|
if (!op || !targetPath) {
|
|
@@ -4037,6 +4880,11 @@ async function hasSecretTool() {
|
|
|
4037
4880
|
}
|
|
4038
4881
|
}
|
|
4039
4882
|
async function detectBackend() {
|
|
4883
|
+
const override = process.env.CC_AUTH_BACKEND;
|
|
4884
|
+
if (override === "file") return "file";
|
|
4885
|
+
if (override === "keychain-macos") return "keychain-macos";
|
|
4886
|
+
if (override === "keychain-linux") return "keychain-linux";
|
|
4887
|
+
if (override === "keychain-windows") return "keychain-windows";
|
|
4040
4888
|
if (process.platform === "darwin") return "keychain-macos";
|
|
4041
4889
|
if (process.platform === "win32") return "keychain-windows";
|
|
4042
4890
|
if (process.platform === "linux" && await hasSecretTool()) return "keychain-linux";
|
|
@@ -4477,7 +5325,20 @@ function loadCatalog() {
|
|
|
4477
5325
|
cachedCatalog = JSON.parse(readFileSync9(path, "utf8"));
|
|
4478
5326
|
return cachedCatalog;
|
|
4479
5327
|
}
|
|
4480
|
-
function
|
|
5328
|
+
function resolveFramework(root) {
|
|
5329
|
+
if (hasState(root)) {
|
|
5330
|
+
const state2 = readState(root);
|
|
5331
|
+
if (state2?.framework) return state2.framework;
|
|
5332
|
+
}
|
|
5333
|
+
const cfg = readConfig(root);
|
|
5334
|
+
if (cfg?.framework) return cfg.framework;
|
|
5335
|
+
return "reactjs";
|
|
5336
|
+
}
|
|
5337
|
+
function isRn(framework) {
|
|
5338
|
+
return framework === "expo" || framework === "react-native";
|
|
5339
|
+
}
|
|
5340
|
+
function nextStepsForFeature(feature, framework = "reactjs") {
|
|
5341
|
+
const rn = isRn(framework);
|
|
4481
5342
|
switch (feature.type) {
|
|
4482
5343
|
case "default":
|
|
4483
5344
|
return [
|
|
@@ -4520,19 +5381,44 @@ function nextStepsForFeature(feature) {
|
|
|
4520
5381
|
}
|
|
4521
5382
|
return lines;
|
|
4522
5383
|
}
|
|
4523
|
-
case "package-install":
|
|
4524
|
-
|
|
5384
|
+
case "package-install": {
|
|
5385
|
+
const pkg = rn && feature.package_native ? feature.package_native : feature.package;
|
|
5386
|
+
const peers = rn ? feature.package_native_peers ?? [] : [];
|
|
5387
|
+
const installCmd = framework === "expo" ? `npx expo install ${[pkg, ...peers].filter(Boolean).join(" ")}` : `npm install ${[pkg, ...peers].filter(Boolean).join(" ")}`;
|
|
5388
|
+
const restartCmd = framework === "expo" ? "npx expo start --clear" : rn ? "npm start -- --reset-cache" : "npm run dev";
|
|
5389
|
+
const docsPath = rn ? "react-native" : "react";
|
|
5390
|
+
const baseSteps = [
|
|
4525
5391
|
`${feature.name} requires installing an additional npm package.`,
|
|
4526
5392
|
"",
|
|
4527
5393
|
"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}`] : []
|
|
5394
|
+
` 1. Install:`,
|
|
5395
|
+
` ${installCmd}`
|
|
4535
5396
|
];
|
|
5397
|
+
if (framework === "react-native") {
|
|
5398
|
+
baseSteps.push(` 2. Run \`cd ios && pod install && cd ..\` to link the native modules.`);
|
|
5399
|
+
baseSteps.push(` 3. Restart Metro with \`${restartCmd}\`. After the next login, the UI Kit's initiateAfterLogin() auto-wires the CometChat calling listeners.`);
|
|
5400
|
+
baseSteps.push(` 4. Call buttons appear in CometChatMessageHeader; incoming calls render via the CometChatIncomingCall listener.`);
|
|
5401
|
+
} else if (framework === "expo") {
|
|
5402
|
+
baseSteps.push(` 2. Run \`npx expo prebuild --clean\` (calling requires a dev build \u2014 Expo Go cannot load WebRTC).`);
|
|
5403
|
+
baseSteps.push(` 3. Restart Metro with \`${restartCmd}\`. After the next login, the UI Kit's initiateAfterLogin() auto-wires the CometChat calling listeners.`);
|
|
5404
|
+
baseSteps.push(` 4. Call buttons appear in CometChatMessageHeader; incoming calls render via the CometChatIncomingCall listener.`);
|
|
5405
|
+
} else {
|
|
5406
|
+
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.`);
|
|
5407
|
+
baseSteps.push(` 3. Call buttons appear in CometChatMessageHeader; incoming calls render via the global call listener.`);
|
|
5408
|
+
}
|
|
5409
|
+
if (feature.components && feature.components.length > 0) {
|
|
5410
|
+
baseSteps.push(` Components automatically enabled: ${feature.components.join(", ")}`);
|
|
5411
|
+
}
|
|
5412
|
+
if (rn) {
|
|
5413
|
+
baseSteps.push(` Note: on React Native you customize calling UI via custom view templates (see cometchat-native-features skill \xA7 Calls).`);
|
|
5414
|
+
} else {
|
|
5415
|
+
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.`);
|
|
5416
|
+
}
|
|
5417
|
+
if (feature.docs_topic) {
|
|
5418
|
+
baseSteps.push(` Docs: https://www.cometchat.com/docs/ui-kit/${docsPath}/${feature.docs_topic}`);
|
|
5419
|
+
}
|
|
5420
|
+
return baseSteps;
|
|
5421
|
+
}
|
|
4536
5422
|
case "component-swap":
|
|
4537
5423
|
return [
|
|
4538
5424
|
`${feature.name} is enabled by replacing one component with a variant.`,
|
|
@@ -4811,10 +5697,12 @@ function featuresInfo(args, name) {
|
|
|
4811
5697
|
}
|
|
4812
5698
|
return 1;
|
|
4813
5699
|
}
|
|
5700
|
+
const root = projectPath10(args);
|
|
5701
|
+
const framework = resolveFramework(root);
|
|
4814
5702
|
const result = {
|
|
4815
5703
|
status: "found",
|
|
4816
5704
|
feature: fuzzy,
|
|
4817
|
-
next_steps: nextStepsForFeature(fuzzy)
|
|
5705
|
+
next_steps: nextStepsForFeature(fuzzy, framework)
|
|
4818
5706
|
};
|
|
4819
5707
|
if (isJsonMode11(args)) {
|
|
4820
5708
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -4873,8 +5761,11 @@ function projectPath11(args) {
|
|
|
4873
5761
|
if (typeof fromFlag === "string") return resolve12(fromFlag);
|
|
4874
5762
|
return resolve12(process.cwd());
|
|
4875
5763
|
}
|
|
4876
|
-
function runQuickVerify(root, ownedFiles, patchedFiles = []) {
|
|
5764
|
+
function runQuickVerify(root, ownedFiles, patchedFiles = [], framework = null) {
|
|
4877
5765
|
if (ownedFiles.length === 0) return { status: "skip", failed: [] };
|
|
5766
|
+
if (framework === "expo" || framework === "react-native") {
|
|
5767
|
+
return runQuickVerifyRn(root, ownedFiles, patchedFiles, framework);
|
|
5768
|
+
}
|
|
4878
5769
|
const failed = [];
|
|
4879
5770
|
const HEX_KEY = /[a-f0-9]{32,}/;
|
|
4880
5771
|
const cssCandidates = /* @__PURE__ */ new Set([
|
|
@@ -4917,6 +5808,69 @@ function runQuickVerify(root, ownedFiles, patchedFiles = []) {
|
|
|
4917
5808
|
if (!errorUiFound) failed.push("error_ui_visible_on_failure");
|
|
4918
5809
|
return { status: failed.length === 0 ? "pass" : "fail", failed };
|
|
4919
5810
|
}
|
|
5811
|
+
function runQuickVerifyRn(root, ownedFiles, patchedFiles, framework) {
|
|
5812
|
+
const failed = [];
|
|
5813
|
+
const HEX_KEY = /[a-f0-9]{32,}/;
|
|
5814
|
+
const allFiles = /* @__PURE__ */ new Set([...ownedFiles, ...patchedFiles]);
|
|
5815
|
+
const entryCandidates = ["index.js", "index.ts", "App.tsx", "App.jsx", "app/_layout.tsx", "app/_layout.js"];
|
|
5816
|
+
let gestureHandlerLine1 = false;
|
|
5817
|
+
for (const e of entryCandidates) {
|
|
5818
|
+
if (!pathExists(p(root, e))) continue;
|
|
5819
|
+
const content = readFileOrNull(p(root, e));
|
|
5820
|
+
if (!content) continue;
|
|
5821
|
+
const firstLine = content.split(/\r?\n/)[0]?.trim() ?? "";
|
|
5822
|
+
if (/^import\s+['"]react-native-gesture-handler['"]/.test(firstLine)) {
|
|
5823
|
+
gestureHandlerLine1 = true;
|
|
5824
|
+
break;
|
|
5825
|
+
}
|
|
5826
|
+
}
|
|
5827
|
+
if (!gestureHandlerLine1) failed.push("gesture_handler_line_1");
|
|
5828
|
+
const wrappers = ["GestureHandlerRootView", "SafeAreaProvider", "CometChatThemeProvider", "CometChatProvider"];
|
|
5829
|
+
const wrapperHits = /* @__PURE__ */ new Set();
|
|
5830
|
+
for (const file of allFiles) {
|
|
5831
|
+
const content = readFileOrNull(p(root, file));
|
|
5832
|
+
if (!content) continue;
|
|
5833
|
+
for (const w of wrappers) {
|
|
5834
|
+
if (content.includes(w)) wrapperHits.add(w);
|
|
5835
|
+
}
|
|
5836
|
+
}
|
|
5837
|
+
if (wrapperHits.size < wrappers.length) failed.push("four_wrapper_chain_present");
|
|
5838
|
+
let hasThreadPanel = false;
|
|
5839
|
+
for (const file of allFiles) {
|
|
5840
|
+
const content = readFileOrNull(p(root, file));
|
|
5841
|
+
if (content && content.includes("CometChatThreadHeader")) {
|
|
5842
|
+
hasThreadPanel = true;
|
|
5843
|
+
break;
|
|
5844
|
+
}
|
|
5845
|
+
}
|
|
5846
|
+
if (!hasThreadPanel) {
|
|
5847
|
+
for (const file of ownedFiles) {
|
|
5848
|
+
const content = readFileOrNull(p(root, file));
|
|
5849
|
+
if (!content) continue;
|
|
5850
|
+
const lists = (content.match(/<\s*CometChatMessageList\b/g) ?? []).length;
|
|
5851
|
+
const flagged = (content.match(/hideReplyInThreadOption/g) ?? []).length;
|
|
5852
|
+
if (lists > flagged) {
|
|
5853
|
+
failed.push("hide_reply_in_thread_option");
|
|
5854
|
+
break;
|
|
5855
|
+
}
|
|
5856
|
+
}
|
|
5857
|
+
}
|
|
5858
|
+
for (const file of ownedFiles) {
|
|
5859
|
+
const content = readFileOrNull(p(root, file));
|
|
5860
|
+
if (!content) continue;
|
|
5861
|
+
const m = content.match(HEX_KEY);
|
|
5862
|
+
if (m && m[0].length >= 40) {
|
|
5863
|
+
failed.push("no_auth_key_in_source");
|
|
5864
|
+
break;
|
|
5865
|
+
}
|
|
5866
|
+
}
|
|
5867
|
+
if (framework === "react-native" && pathExists(p(root, "ios/Podfile"))) {
|
|
5868
|
+
if (!pathExists(p(root, "ios/Podfile.lock"))) {
|
|
5869
|
+
failed.push("pod_install_run");
|
|
5870
|
+
}
|
|
5871
|
+
}
|
|
5872
|
+
return { status: failed.length === 0 ? "pass" : "fail", failed };
|
|
5873
|
+
}
|
|
4920
5874
|
function buildIssues(args) {
|
|
4921
5875
|
const issues = [];
|
|
4922
5876
|
if (!args.hasIntegration) {
|
|
@@ -4964,6 +5918,23 @@ function buildIssues(args) {
|
|
|
4964
5918
|
error_ui_visible_on_failure: {
|
|
4965
5919
|
msg: "No visible error UI (color: red) found in any owned file.",
|
|
4966
5920
|
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."
|
|
5921
|
+
},
|
|
5922
|
+
// ── React Native checks ─────────────────────────────────────────────────
|
|
5923
|
+
gesture_handler_line_1: {
|
|
5924
|
+
msg: '`import "react-native-gesture-handler"` is not line 1 of the entry file (index.js / App.tsx / app/_layout.tsx).',
|
|
5925
|
+
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.'
|
|
5926
|
+
},
|
|
5927
|
+
four_wrapper_chain_present: {
|
|
5928
|
+
msg: "One or more of the 4 required wrappers is missing: GestureHandlerRootView \u2192 SafeAreaProvider \u2192 CometChatThemeProvider \u2192 CometChatProvider.",
|
|
5929
|
+
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."
|
|
5930
|
+
},
|
|
5931
|
+
hide_reply_in_thread_option: {
|
|
5932
|
+
msg: "A <CometChatMessageList> was found without `hideReplyInThreadOption`, and no <CometChatThreadHeader> is wired.",
|
|
5933
|
+
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.'
|
|
5934
|
+
},
|
|
5935
|
+
pod_install_run: {
|
|
5936
|
+
msg: "ios/Podfile exists but ios/Podfile.lock is missing \u2014 pod install has not been run.",
|
|
5937
|
+
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
5938
|
}
|
|
4968
5939
|
};
|
|
4969
5940
|
for (const failed of args.verify.failed) {
|
|
@@ -5029,7 +6000,8 @@ async function doctor(args) {
|
|
|
5029
6000
|
verifyResult = runQuickVerify(
|
|
5030
6001
|
root,
|
|
5031
6002
|
stateInfo.files_owned,
|
|
5032
|
-
stateInfo.files_patched.map((p2) => p2.path)
|
|
6003
|
+
stateInfo.files_patched.map((p2) => p2.path),
|
|
6004
|
+
stateInfo.framework
|
|
5033
6005
|
);
|
|
5034
6006
|
}
|
|
5035
6007
|
const expectedEnvVars = detected.env_prefix !== null ? [
|
|
@@ -5544,6 +6516,7 @@ function printHumanReadable12(r) {
|
|
|
5544
6516
|
init_state();
|
|
5545
6517
|
import { readFileSync as readFileSync11 } from "node:fs";
|
|
5546
6518
|
import { join as join14, resolve as resolve15 } from "node:path";
|
|
6519
|
+
init_config();
|
|
5547
6520
|
var HELP14 = `
|
|
5548
6521
|
cometchat add-user-mgmt \u2014 create server-side user management endpoints
|
|
5549
6522
|
|
|
@@ -5589,13 +6562,28 @@ async function addUserMgmt(args) {
|
|
|
5589
6562
|
return 0;
|
|
5590
6563
|
}
|
|
5591
6564
|
const root = projectPath14(args);
|
|
5592
|
-
|
|
6565
|
+
const stateFramework = hasState(root) ? readState(root)?.framework ?? null : null;
|
|
6566
|
+
const cfgFramework = readConfig(root)?.framework ?? null;
|
|
6567
|
+
const resolvedFramework = stateFramework ?? cfgFramework;
|
|
6568
|
+
if (!resolvedFramework) {
|
|
5593
6569
|
return outputResult10(args, {
|
|
5594
6570
|
status: "no-integration",
|
|
5595
6571
|
framework: "",
|
|
5596
6572
|
files_created: [],
|
|
5597
6573
|
next_steps: [],
|
|
5598
|
-
error: "API_ERROR: ERR_NO_INTEGRATION No integration found. Run `cometchat apply`
|
|
6574
|
+
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`."
|
|
6575
|
+
}, 1);
|
|
6576
|
+
}
|
|
6577
|
+
if (resolvedFramework === "expo" || resolvedFramework === "react-native") {
|
|
6578
|
+
return addUserMgmtRn(args, root, resolvedFramework);
|
|
6579
|
+
}
|
|
6580
|
+
if (!hasState(root)) {
|
|
6581
|
+
return outputResult10(args, {
|
|
6582
|
+
status: "no-integration",
|
|
6583
|
+
framework: resolvedFramework,
|
|
6584
|
+
files_created: [],
|
|
6585
|
+
next_steps: [],
|
|
6586
|
+
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
6587
|
}, 1);
|
|
5600
6588
|
}
|
|
5601
6589
|
const state2 = readState(root);
|
|
@@ -5742,6 +6730,74 @@ function printHumanReadable13(r) {
|
|
|
5742
6730
|
lines.push("");
|
|
5743
6731
|
console.log(lines.join("\n"));
|
|
5744
6732
|
}
|
|
6733
|
+
function addUserMgmtRn(args, root, framework) {
|
|
6734
|
+
const backendFlag = args.flags.backend;
|
|
6735
|
+
const outDirFlag = args.flags["out-dir"];
|
|
6736
|
+
if (typeof backendFlag !== "string" || !RN_BACKENDS.includes(backendFlag)) {
|
|
6737
|
+
return outputResult10(args, {
|
|
6738
|
+
status: "error",
|
|
6739
|
+
framework,
|
|
6740
|
+
files_created: [],
|
|
6741
|
+
next_steps: [],
|
|
6742
|
+
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\`.`
|
|
6743
|
+
}, 1);
|
|
6744
|
+
}
|
|
6745
|
+
const backend = backendFlag;
|
|
6746
|
+
const tpl = rnUserMgmtTemplate(backend);
|
|
6747
|
+
const defaultOutDir = backend === "firebase-functions" ? "functions/src" : backend === "nextjs-api" ? "src/app/api/cometchat-user" : "server";
|
|
6748
|
+
const outDir = typeof outDirFlag === "string" ? outDirFlag : defaultOutDir;
|
|
6749
|
+
const filePath = join14(outDir, tpl.filename);
|
|
6750
|
+
let writeResult;
|
|
6751
|
+
try {
|
|
6752
|
+
writeResult = applyWrites(root, [
|
|
6753
|
+
{ type: "create", path: filePath, content: tpl.content, owned: true }
|
|
6754
|
+
]);
|
|
6755
|
+
} catch (err) {
|
|
6756
|
+
return errorOut7(args, "RN add-user-mgmt apply failed: " + (err instanceof Error ? err.message : String(err)));
|
|
6757
|
+
}
|
|
6758
|
+
if (writeResult.files_skipped.length > 0) {
|
|
6759
|
+
return outputResult10(args, {
|
|
6760
|
+
status: "already-applied",
|
|
6761
|
+
framework,
|
|
6762
|
+
files_created: [],
|
|
6763
|
+
next_steps: [
|
|
6764
|
+
`Skipped: ${filePath} already exists. Delete it to regenerate.`
|
|
6765
|
+
],
|
|
6766
|
+
error: `API_ERROR: ERR_ALREADY_APPLIED ${filePath} already exists.`
|
|
6767
|
+
}, 0);
|
|
6768
|
+
}
|
|
6769
|
+
const next_steps = [
|
|
6770
|
+
`1. Backend deps: ${tpl.deps.length > 0 ? `\`npm install ${tpl.deps.join(" ")}\` in ${outDir.split("/")[0] ?? outDir}` : "(none \u2014 endpoint ships ready to deploy)"}.`,
|
|
6771
|
+
`2. Set server env vars (NO EXPO_PUBLIC_ prefix):`,
|
|
6772
|
+
` COMETCHAT_APP_ID=<your-app-id>`,
|
|
6773
|
+
` COMETCHAT_REGION=<us|eu|in>`,
|
|
6774
|
+
` COMETCHAT_AUTH_KEY=<your-auth-key> # SERVER-ONLY`,
|
|
6775
|
+
`3. Start the backend: ${tpl.runCommand ?? "(deploy per platform convention)"}`,
|
|
6776
|
+
`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.`,
|
|
6777
|
+
`5. Wire the endpoint into your RN app's flows:`,
|
|
6778
|
+
` - On signup (after your auth provider creates a user): POST /cometchat-user with { uid, name, avatar? }`,
|
|
6779
|
+
` - On profile update: PATCH /cometchat-user/<uid> with the changed fields`,
|
|
6780
|
+
` - On account delete: DELETE /cometchat-user/<uid>`,
|
|
6781
|
+
` See cometchat-native-production skill \xA7 6 for Firebase Auth / Supabase / Clerk / Auth0 recipes.`
|
|
6782
|
+
];
|
|
6783
|
+
appendAuditEntry(root, {
|
|
6784
|
+
command: "add-user-mgmt",
|
|
6785
|
+
summary: `Scaffolded ${tpl.label} at ${filePath}`,
|
|
6786
|
+
inputs: { framework, backend, "out-dir": outDir },
|
|
6787
|
+
decisions: {
|
|
6788
|
+
backend: `${tpl.label}. Ships unauthenticated \u2014 caller must add session checks.`,
|
|
6789
|
+
file: `Wrote ${filePath} (owned). Deps: ${tpl.deps.length > 0 ? tpl.deps.join(", ") : "none"}.`
|
|
6790
|
+
},
|
|
6791
|
+
files_patched: [filePath],
|
|
6792
|
+
next_actions: next_steps
|
|
6793
|
+
});
|
|
6794
|
+
return outputResult10(args, {
|
|
6795
|
+
status: "applied",
|
|
6796
|
+
framework,
|
|
6797
|
+
files_created: [filePath],
|
|
6798
|
+
next_steps
|
|
6799
|
+
}, 0);
|
|
6800
|
+
}
|
|
5745
6801
|
|
|
5746
6802
|
// src/commands/apply-feature.ts
|
|
5747
6803
|
init_state();
|
|
@@ -6711,6 +7767,8 @@ function stripNoise(result) {
|
|
|
6711
7767
|
return result;
|
|
6712
7768
|
}
|
|
6713
7769
|
const { apiHost: _h, backend: _b, ...rest } = result;
|
|
7770
|
+
void _h;
|
|
7771
|
+
void _b;
|
|
6714
7772
|
return rest;
|
|
6715
7773
|
}
|
|
6716
7774
|
function emit(json, result) {
|
|
@@ -6876,7 +7934,7 @@ async function provisionSetup(args) {
|
|
|
6876
7934
|
return 1;
|
|
6877
7935
|
}
|
|
6878
7936
|
if (!frameworkFlag) {
|
|
6879
|
-
const msg = "Missing --framework. One of: reactjs, nextjs, react-router, astro.";
|
|
7937
|
+
const msg = "Missing --framework. One of: reactjs, nextjs, react-router, astro, expo, react-native.";
|
|
6880
7938
|
if (json) return emitError(true, msg);
|
|
6881
7939
|
console.error(msg);
|
|
6882
7940
|
return 1;
|
|
@@ -6965,7 +8023,10 @@ var ENV_PREFIX_BY_FRAMEWORK = {
|
|
|
6965
8023
|
reactjs: "VITE_",
|
|
6966
8024
|
nextjs: "NEXT_PUBLIC_",
|
|
6967
8025
|
"react-router": "VITE_",
|
|
6968
|
-
astro: "PUBLIC_"
|
|
8026
|
+
astro: "PUBLIC_",
|
|
8027
|
+
expo: "EXPO_PUBLIC_",
|
|
8028
|
+
"react-native": ""
|
|
8029
|
+
// bare RN uses react-native-dotenv — no prefix
|
|
6969
8030
|
};
|
|
6970
8031
|
async function provisionList(args) {
|
|
6971
8032
|
const json = isJsonMode19(args);
|
|
@@ -8437,6 +9498,14 @@ function projectPath18(args) {
|
|
|
8437
9498
|
if (typeof fromFlag === "string") return resolve21(fromFlag);
|
|
8438
9499
|
return resolve21(process.cwd());
|
|
8439
9500
|
}
|
|
9501
|
+
var STATE_ENV_PREFIX_BY_FRAMEWORK = {
|
|
9502
|
+
reactjs: "VITE_",
|
|
9503
|
+
nextjs: "NEXT_PUBLIC_",
|
|
9504
|
+
"react-router": "VITE_",
|
|
9505
|
+
astro: "PUBLIC_",
|
|
9506
|
+
expo: "EXPO_PUBLIC_",
|
|
9507
|
+
"react-native": ""
|
|
9508
|
+
};
|
|
8440
9509
|
async function state(args) {
|
|
8441
9510
|
if (args.flags.help || args.flags.h) {
|
|
8442
9511
|
console.log(HELP21);
|
|
@@ -8474,7 +9543,7 @@ async function stateRecord(args) {
|
|
|
8474
9543
|
return 1;
|
|
8475
9544
|
}
|
|
8476
9545
|
if (!envPrefix) {
|
|
8477
|
-
envPrefix = framework
|
|
9546
|
+
envPrefix = STATE_ENV_PREFIX_BY_FRAMEWORK[framework] ?? "VITE_";
|
|
8478
9547
|
}
|
|
8479
9548
|
const experienceStr = asString5(args.flags.experience) ?? "0";
|
|
8480
9549
|
const experience = parseInt(experienceStr, 10);
|