@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/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 v2 supports: reactjs (Vite/CRA), nextjs, react-router (v6/v7), astro."
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 checks = {
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
- Replaces client-side login(UID) + setAuthKey() with a server-side token
3348
- endpoint so your AUTH_KEY never reaches the browser. Creates the API
3349
- route file and auto-rewrites the canonical client login chain into a
3350
- fetch('/api/cometchat-token?uid=...') \u2192 loginWithAuthToken flow. If the
3351
- client file was customized, the auto-patch is skipped and manual steps
3352
- are surfaced. Supports nextjs, react-router, and astro.
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
- --path <p> Project root (defaults to cwd).
3356
- --json Machine-readable JSON output.
3357
- --help, -h Show this help.
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
- if (!hasState(root)) {
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 to upgrade to production-ready auth."
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 nextStepsForFeature(feature) {
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
- return [
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 the package:`,
4529
- ` npm install ${feature.package}`,
4530
- ` 2. Restart your dev server. After the next login, the UI Kit's initiateAfterLogin() automatically calls enableCalling(), which detects the calls SDK and wires up the default CallingExtension. No manual setExtensions() or setCallsExtension() needed for the default behavior.`,
4531
- ` 3. Call buttons appear in CometChatMessageHeader; incoming calls render via the global call listener.`,
4532
- ...feature.components && feature.components.length > 0 ? [` 4. Components automatically enabled: ${feature.components.join(", ")}`] : [],
4533
- ` Note: to customize the calling UI (custom CallingExtension, group calls config, recording), pass your own instance via UIKitSettingsBuilder.setCallsExtension(new CallingExtension({...})) before init.`,
4534
- ...feature.docs_topic ? [` Docs: https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}`] : []
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
- if (!hasState(root)) {
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` first to create the base integration, then run `cometchat add-user-mgmt`."
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 === "nextjs" ? "NEXT_PUBLIC_" : framework === "astro" ? "PUBLIC_" : "VITE_";
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);