@cometchat/skills-cli 2.0.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/README.md +70 -0
- package/bin/cometchat.mjs +7 -0
- package/dist/index.js +4395 -0
- package/dist/registry/schemas/template.schema.json +142 -0
- package/dist/registry/v6/experiences/1-conversation-list/astro.template.json +89 -0
- package/dist/registry/v6/experiences/1-conversation-list/nextjs-pages.template.json +83 -0
- package/dist/registry/v6/experiences/1-conversation-list/nextjs.template.json +83 -0
- package/dist/registry/v6/experiences/1-conversation-list/react-router.template.json +102 -0
- package/dist/registry/v6/experiences/1-conversation-list/reactjs.template.json +86 -0
- package/dist/registry/v6/experiences/1-conversation-list/templates/astro/ChatApp.css.tpl +39 -0
- package/dist/registry/v6/experiences/1-conversation-list/templates/astro/ChatApp.tsx.tpl +86 -0
- package/dist/registry/v6/experiences/1-conversation-list/templates/astro/CometChatSelector.tsx.tpl +40 -0
- package/dist/registry/v6/experiences/1-conversation-list/templates/astro/chat.astro.tpl +17 -0
- package/dist/registry/v6/experiences/1-conversation-list/templates/nextjs/CometChatNoSSR.css.tpl +39 -0
- package/dist/registry/v6/experiences/1-conversation-list/templates/nextjs/CometChatNoSSR.tsx.tpl +86 -0
- package/dist/registry/v6/experiences/1-conversation-list/templates/nextjs/CometChatSelector.tsx.tpl +41 -0
- package/dist/registry/v6/experiences/1-conversation-list/templates/nextjs/chat-page.tsx.tpl +11 -0
- package/dist/registry/v6/experiences/1-conversation-list/templates/nextjs-pages/CometChatNoSSR.css.tpl +39 -0
- package/dist/registry/v6/experiences/1-conversation-list/templates/nextjs-pages/CometChatNoSSR.tsx.tpl +88 -0
- package/dist/registry/v6/experiences/1-conversation-list/templates/nextjs-pages/CometChatSelector.tsx.tpl +41 -0
- package/dist/registry/v6/experiences/1-conversation-list/templates/nextjs-pages/chat-page.tsx.tpl +15 -0
- package/dist/registry/v6/experiences/1-conversation-list/templates/react-router/CometChat-route.tsx.tpl +22 -0
- package/dist/registry/v6/experiences/1-conversation-list/templates/react-router/CometChatNoSSR.css.tpl +39 -0
- package/dist/registry/v6/experiences/1-conversation-list/templates/react-router/CometChatNoSSR.tsx.tpl +87 -0
- package/dist/registry/v6/experiences/1-conversation-list/templates/react-router/CometChatSelector.tsx.tpl +40 -0
- package/dist/registry/v6/experiences/1-conversation-list/templates/reactjs/App.css.tpl +32 -0
- package/dist/registry/v6/experiences/1-conversation-list/templates/reactjs/App.tsx.tpl +55 -0
- package/dist/registry/v6/experiences/1-conversation-list/templates/reactjs/CometChatSelector.tsx.tpl +40 -0
- package/dist/registry/v6/experiences/1-conversation-list/templates/reactjs/main.tsx.tpl +46 -0
- package/dist/registry/v6/experiences/2-one-to-one/astro.template.json +81 -0
- package/dist/registry/v6/experiences/2-one-to-one/nextjs.template.json +77 -0
- package/dist/registry/v6/experiences/2-one-to-one/react-router.template.json +93 -0
- package/dist/registry/v6/experiences/2-one-to-one/reactjs.template.json +77 -0
- package/dist/registry/v6/experiences/2-one-to-one/templates/astro/ChatApp.css.tpl +21 -0
- package/dist/registry/v6/experiences/2-one-to-one/templates/astro/ChatApp.tsx.tpl +66 -0
- package/dist/registry/v6/experiences/2-one-to-one/templates/astro/chat.astro.tpl +17 -0
- package/dist/registry/v6/experiences/2-one-to-one/templates/nextjs/CometChatNoSSR.css.tpl +21 -0
- package/dist/registry/v6/experiences/2-one-to-one/templates/nextjs/CometChatNoSSR.tsx.tpl +66 -0
- package/dist/registry/v6/experiences/2-one-to-one/templates/nextjs/chat-page.tsx.tpl +11 -0
- package/dist/registry/v6/experiences/2-one-to-one/templates/react-router/CometChat-route.tsx.tpl +21 -0
- package/dist/registry/v6/experiences/2-one-to-one/templates/react-router/CometChatNoSSR.css.tpl +21 -0
- package/dist/registry/v6/experiences/2-one-to-one/templates/react-router/CometChatNoSSR.tsx.tpl +67 -0
- package/dist/registry/v6/experiences/2-one-to-one/templates/reactjs/App.css.tpl +4 -0
- package/dist/registry/v6/experiences/2-one-to-one/templates/reactjs/App.tsx.tpl +44 -0
- package/dist/registry/v6/experiences/2-one-to-one/templates/reactjs/main.tsx.tpl +46 -0
- package/dist/registry/v6/experiences/3-tab-based/astro.template.json +93 -0
- package/dist/registry/v6/experiences/3-tab-based/nextjs.template.json +90 -0
- package/dist/registry/v6/experiences/3-tab-based/react-router.template.json +105 -0
- package/dist/registry/v6/experiences/3-tab-based/reactjs.template.json +90 -0
- package/dist/registry/v6/experiences/3-tab-based/templates/astro/ChatApp.css.tpl +39 -0
- package/dist/registry/v6/experiences/3-tab-based/templates/astro/ChatApp.tsx.tpl +93 -0
- package/dist/registry/v6/experiences/3-tab-based/templates/astro/CometChatTabs.css.tpl +43 -0
- package/dist/registry/v6/experiences/3-tab-based/templates/astro/CometChatTabs.tsx.tpl +72 -0
- package/dist/registry/v6/experiences/3-tab-based/templates/astro/chat.astro.tpl +17 -0
- package/dist/registry/v6/experiences/3-tab-based/templates/nextjs/CometChatNoSSR.css.tpl +39 -0
- package/dist/registry/v6/experiences/3-tab-based/templates/nextjs/CometChatNoSSR.tsx.tpl +93 -0
- package/dist/registry/v6/experiences/3-tab-based/templates/nextjs/CometChatTabs.css.tpl +43 -0
- package/dist/registry/v6/experiences/3-tab-based/templates/nextjs/CometChatTabs.tsx.tpl +73 -0
- package/dist/registry/v6/experiences/3-tab-based/templates/nextjs/chat-page.tsx.tpl +11 -0
- package/dist/registry/v6/experiences/3-tab-based/templates/react-router/CometChat-route.tsx.tpl +21 -0
- package/dist/registry/v6/experiences/3-tab-based/templates/react-router/CometChatNoSSR.css.tpl +39 -0
- package/dist/registry/v6/experiences/3-tab-based/templates/react-router/CometChatNoSSR.tsx.tpl +94 -0
- package/dist/registry/v6/experiences/3-tab-based/templates/react-router/CometChatTabs.css.tpl +43 -0
- package/dist/registry/v6/experiences/3-tab-based/templates/react-router/CometChatTabs.tsx.tpl +72 -0
- package/dist/registry/v6/experiences/3-tab-based/templates/reactjs/App.css.tpl +7 -0
- package/dist/registry/v6/experiences/3-tab-based/templates/reactjs/App.tsx.tpl +62 -0
- package/dist/registry/v6/experiences/3-tab-based/templates/reactjs/CometChatTabs.css.tpl +43 -0
- package/dist/registry/v6/experiences/3-tab-based/templates/reactjs/CometChatTabs.tsx.tpl +72 -0
- package/dist/registry/v6/experiences/3-tab-based/templates/reactjs/main.tsx.tpl +46 -0
- package/dist/registry/v6/features/catalog.json +344 -0
- package/dist/registry/v6/production/astro/cometchat-token.ts.tpl +86 -0
- package/dist/registry/v6/production/astro.template.json +51 -0
- package/dist/registry/v6/production/nextjs/route.ts.tpl +79 -0
- package/dist/registry/v6/production/nextjs.template.json +50 -0
- package/dist/registry/v6/production/react-router/api.cometchat-token.ts.tpl +73 -0
- package/dist/registry/v6/production/react-router.template.json +58 -0
- package/dist/registry/v6/user-mgmt/astro/cometchat-user.ts.tpl +164 -0
- package/dist/registry/v6/user-mgmt/astro.template.json +33 -0
- package/dist/registry/v6/user-mgmt/nextjs/route.ts.tpl +212 -0
- package/dist/registry/v6/user-mgmt/nextjs.template.json +57 -0
- package/dist/registry/v6/user-mgmt/react-router/api.cometchat-user.ts.tpl +176 -0
- package/dist/registry/v6/user-mgmt/react-router.template.json +40 -0
- package/dist/registry/v6/widget/astro/CometChatWidget.css.tpl +105 -0
- package/dist/registry/v6/widget/astro/CometChatWidget.tsx.tpl +88 -0
- package/dist/registry/v6/widget/astro.template.json +38 -0
- package/dist/registry/v6/widget/nextjs/CometChatWidget.css.tpl +105 -0
- package/dist/registry/v6/widget/nextjs/CometChatWidget.tsx.tpl +88 -0
- package/dist/registry/v6/widget/nextjs.template.json +40 -0
- package/dist/registry/v6/widget/react-router/CometChatWidget.css.tpl +105 -0
- package/dist/registry/v6/widget/react-router/CometChatWidget.tsx.tpl +98 -0
- package/dist/registry/v6/widget/react-router.template.json +36 -0
- package/dist/registry/v6/widget/reactjs/CometChatWidget.css.tpl +105 -0
- package/dist/registry/v6/widget/reactjs/CometChatWidget.tsx.tpl +89 -0
- package/dist/registry/v6/widget/reactjs.template.json +36 -0
- package/package.json +53 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4395 @@
|
|
|
1
|
+
// src/commands/detect.ts
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
// src/utils/version.ts
|
|
5
|
+
function stripRange(versionRange) {
|
|
6
|
+
return versionRange.replace(/^[\^~>=<\s]+/, "").trim();
|
|
7
|
+
}
|
|
8
|
+
function extractMajor(versionRange) {
|
|
9
|
+
if (!versionRange) return null;
|
|
10
|
+
const stripped = stripRange(versionRange);
|
|
11
|
+
const match = stripped.match(/^(\d+)/);
|
|
12
|
+
if (!match || !match[1]) return null;
|
|
13
|
+
const n = parseInt(match[1], 10);
|
|
14
|
+
return Number.isFinite(n) ? n : null;
|
|
15
|
+
}
|
|
16
|
+
function extractVersion(versionRange) {
|
|
17
|
+
if (!versionRange) return null;
|
|
18
|
+
const stripped = stripRange(versionRange);
|
|
19
|
+
return stripped || null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/utils/fs.ts
|
|
23
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
function readFileOrNull(path) {
|
|
26
|
+
try {
|
|
27
|
+
if (!existsSync(path)) return null;
|
|
28
|
+
return readFileSync(path, "utf8");
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function readJsonOrNull(path) {
|
|
34
|
+
const content = readFileOrNull(path);
|
|
35
|
+
if (content === null) return null;
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(content);
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function pathExists(path) {
|
|
43
|
+
return existsSync(path);
|
|
44
|
+
}
|
|
45
|
+
function p(root, ...parts) {
|
|
46
|
+
return join(root, ...parts);
|
|
47
|
+
}
|
|
48
|
+
function parseEnvFile(content) {
|
|
49
|
+
const result = {};
|
|
50
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
51
|
+
const line = rawLine.trim();
|
|
52
|
+
if (!line || line.startsWith("#")) continue;
|
|
53
|
+
const eq = line.indexOf("=");
|
|
54
|
+
if (eq === -1) continue;
|
|
55
|
+
const key = line.slice(0, eq).trim();
|
|
56
|
+
let value = line.slice(eq + 1).trim();
|
|
57
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
58
|
+
value = value.slice(1, -1);
|
|
59
|
+
}
|
|
60
|
+
const hash = value.indexOf(" #");
|
|
61
|
+
if (hash !== -1) value = value.slice(0, hash).trim();
|
|
62
|
+
if (key) result[key] = value;
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
function readAllEnvFiles(root) {
|
|
67
|
+
const merged = {};
|
|
68
|
+
const files = [".env", ".env.local", ".env.development", ".env.development.local"];
|
|
69
|
+
for (const f of files) {
|
|
70
|
+
const content = readFileOrNull(p(root, f));
|
|
71
|
+
if (content !== null) {
|
|
72
|
+
Object.assign(merged, parseEnvFile(content));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return merged;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/detectors/framework.ts
|
|
79
|
+
var EMPTY = {
|
|
80
|
+
framework: null,
|
|
81
|
+
framework_version: null,
|
|
82
|
+
router: null,
|
|
83
|
+
bundler: null,
|
|
84
|
+
ssr_strategy: null,
|
|
85
|
+
env_prefix: null
|
|
86
|
+
};
|
|
87
|
+
function allDeps(pkg) {
|
|
88
|
+
return {
|
|
89
|
+
...pkg.dependencies ?? {},
|
|
90
|
+
...pkg.devDependencies ?? {}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function hasViteConfig(root) {
|
|
94
|
+
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"));
|
|
95
|
+
}
|
|
96
|
+
function detectNextjs(root, deps) {
|
|
97
|
+
if (!deps["next"]) return null;
|
|
98
|
+
const version = extractVersion(deps["next"]);
|
|
99
|
+
let router = null;
|
|
100
|
+
const hasAppDir = pathExists(p(root, "app")) || pathExists(p(root, "src/app"));
|
|
101
|
+
const hasPagesDir = pathExists(p(root, "pages")) || pathExists(p(root, "src/pages"));
|
|
102
|
+
if (hasAppDir) router = "app";
|
|
103
|
+
else if (hasPagesDir) router = "pages";
|
|
104
|
+
let bundler = "webpack";
|
|
105
|
+
const nextConfig = readFileOrNull(p(root, "next.config.js")) ?? readFileOrNull(p(root, "next.config.mjs")) ?? readFileOrNull(p(root, "next.config.ts"));
|
|
106
|
+
if (nextConfig && /turbopack|turbo\s*:/i.test(nextConfig)) {
|
|
107
|
+
bundler = "turbopack";
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
framework: "nextjs",
|
|
111
|
+
framework_version: version,
|
|
112
|
+
router,
|
|
113
|
+
bundler,
|
|
114
|
+
ssr_strategy: "ssr-disabled-dynamic",
|
|
115
|
+
env_prefix: "NEXT_PUBLIC_"
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function detectReactRouter(root, deps) {
|
|
119
|
+
const hasDev = !!deps["@react-router/dev"];
|
|
120
|
+
const hasReactRouter = !!deps["react-router"];
|
|
121
|
+
const hasReactRouterDom = !!deps["react-router-dom"];
|
|
122
|
+
if (!hasDev && !hasReactRouter && !hasReactRouterDom) return null;
|
|
123
|
+
let router = null;
|
|
124
|
+
let version = null;
|
|
125
|
+
if (hasDev) {
|
|
126
|
+
router = "v7";
|
|
127
|
+
version = extractVersion(deps["@react-router/dev"]);
|
|
128
|
+
} else if (hasReactRouter) {
|
|
129
|
+
const major = extractMajor(deps["react-router"]);
|
|
130
|
+
router = major !== null && major >= 7 ? "v7" : "v6";
|
|
131
|
+
version = extractVersion(deps["react-router"]);
|
|
132
|
+
} else if (hasReactRouterDom) {
|
|
133
|
+
const major = extractMajor(deps["react-router-dom"]);
|
|
134
|
+
router = major !== null && major >= 7 ? "v7" : "v6";
|
|
135
|
+
version = extractVersion(deps["react-router-dom"]);
|
|
136
|
+
}
|
|
137
|
+
const bundler = hasViteConfig(root) ? "vite" : "vite";
|
|
138
|
+
return {
|
|
139
|
+
framework: "react-router",
|
|
140
|
+
framework_version: version,
|
|
141
|
+
router,
|
|
142
|
+
bundler,
|
|
143
|
+
ssr_strategy: "client-only-component",
|
|
144
|
+
env_prefix: "VITE_"
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function detectAstro(_root, deps) {
|
|
148
|
+
if (!deps["astro"]) return null;
|
|
149
|
+
const version = extractVersion(deps["astro"]);
|
|
150
|
+
return {
|
|
151
|
+
framework: "astro",
|
|
152
|
+
framework_version: version,
|
|
153
|
+
router: null,
|
|
154
|
+
// Astro has its own routing, not relevant to chat integration
|
|
155
|
+
bundler: "vite",
|
|
156
|
+
// Astro uses Vite internally
|
|
157
|
+
ssr_strategy: "client-only-island",
|
|
158
|
+
env_prefix: "PUBLIC_"
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function detectReactjs(root, deps) {
|
|
162
|
+
if (!deps["react"]) return null;
|
|
163
|
+
const version = extractVersion(deps["react"]);
|
|
164
|
+
const isVite = hasViteConfig(root);
|
|
165
|
+
const isCra = !!deps["react-scripts"] || pathExists(p(root, "public/index.html"));
|
|
166
|
+
return {
|
|
167
|
+
framework: "reactjs",
|
|
168
|
+
framework_version: version,
|
|
169
|
+
router: "spa",
|
|
170
|
+
bundler: isVite ? "vite" : isCra ? "webpack" : "vite",
|
|
171
|
+
// default to vite for ambiguous
|
|
172
|
+
ssr_strategy: "spa-no-ssr",
|
|
173
|
+
env_prefix: isVite ? "VITE_" : isCra ? "REACT_APP_" : "VITE_"
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function detectFramework(root) {
|
|
177
|
+
const pkg = readJsonOrNull(p(root, "package.json"));
|
|
178
|
+
if (!pkg) return EMPTY;
|
|
179
|
+
const deps = allDeps(pkg);
|
|
180
|
+
return detectNextjs(root, deps) ?? detectAstro(root, deps) ?? detectReactRouter(root, deps) ?? detectReactjs(root, deps) ?? EMPTY;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/detectors/package-manager.ts
|
|
184
|
+
function detectPackageManager(root) {
|
|
185
|
+
if (pathExists(p(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
186
|
+
if (pathExists(p(root, "yarn.lock"))) return "yarn";
|
|
187
|
+
if (pathExists(p(root, "bun.lockb")) || pathExists(p(root, "bun.lock"))) return "bun";
|
|
188
|
+
if (pathExists(p(root, "package-lock.json"))) return "npm";
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/detectors/credentials.ts
|
|
193
|
+
import { readdirSync, statSync } from "node:fs";
|
|
194
|
+
import { join as join2, extname } from "node:path";
|
|
195
|
+
function envVarsForPrefix(prefix) {
|
|
196
|
+
const p2 = prefix ?? "";
|
|
197
|
+
return {
|
|
198
|
+
appId: `${p2}COMETCHAT_APP_ID`,
|
|
199
|
+
region: `${p2}COMETCHAT_REGION`,
|
|
200
|
+
authKey: `${p2}COMETCHAT_AUTH_KEY`
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function detectFromEnv(root, prefix) {
|
|
204
|
+
const env = readAllEnvFiles(root);
|
|
205
|
+
const { appId, region, authKey } = envVarsForPrefix(prefix);
|
|
206
|
+
return {
|
|
207
|
+
app_id_present: !!env[appId],
|
|
208
|
+
region_present: !!env[region],
|
|
209
|
+
auth_key_present: !!env[authKey]
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function detectTokenEndpoint(root) {
|
|
213
|
+
const candidates = [
|
|
214
|
+
// Next.js App Router
|
|
215
|
+
"src/app/api/cometchat-token/route.ts",
|
|
216
|
+
"src/app/api/cometchat-token/route.js",
|
|
217
|
+
"src/app/api/cometchat-token/route.tsx",
|
|
218
|
+
"app/api/cometchat-token/route.ts",
|
|
219
|
+
"app/api/cometchat-token/route.js",
|
|
220
|
+
// Next.js Pages Router
|
|
221
|
+
"src/pages/api/cometchat-token.ts",
|
|
222
|
+
"src/pages/api/cometchat-token.js",
|
|
223
|
+
"pages/api/cometchat-token.ts",
|
|
224
|
+
"pages/api/cometchat-token.js",
|
|
225
|
+
// Astro
|
|
226
|
+
"src/pages/api/cometchat-token.ts",
|
|
227
|
+
"src/pages/api/cometchat-token.js",
|
|
228
|
+
// React Router v7 framework mode
|
|
229
|
+
"app/routes/api.cometchat-token.ts",
|
|
230
|
+
"app/routes/api.cometchat-token.tsx"
|
|
231
|
+
];
|
|
232
|
+
for (const candidate of candidates) {
|
|
233
|
+
if (pathExists(p(root, candidate))) {
|
|
234
|
+
return "/api/cometchat-token";
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const authCandidates = [
|
|
238
|
+
"src/app/api/auth/[...nextauth]/route.ts",
|
|
239
|
+
"app/api/auth/[...nextauth]/route.ts"
|
|
240
|
+
];
|
|
241
|
+
for (const candidate of authCandidates) {
|
|
242
|
+
if (pathExists(p(root, candidate))) {
|
|
243
|
+
return "/api/auth";
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
function detectInferredFromSource(root) {
|
|
249
|
+
const srcDirs = ["src", "app"];
|
|
250
|
+
for (const dir of srcDirs) {
|
|
251
|
+
const fullDir = p(root, dir);
|
|
252
|
+
if (!pathExists(fullDir)) continue;
|
|
253
|
+
if (searchForInit(fullDir, 4)) return true;
|
|
254
|
+
}
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
function searchForInit(dir, maxDepth) {
|
|
258
|
+
if (maxDepth < 0) return false;
|
|
259
|
+
let entries;
|
|
260
|
+
try {
|
|
261
|
+
entries = readdirSync(dir);
|
|
262
|
+
} catch {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
for (const entry of entries) {
|
|
266
|
+
if (entry === "node_modules" || entry.startsWith(".")) continue;
|
|
267
|
+
const full = join2(dir, entry);
|
|
268
|
+
let stat;
|
|
269
|
+
try {
|
|
270
|
+
stat = statSync(full);
|
|
271
|
+
} catch {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (stat.isDirectory()) {
|
|
275
|
+
if (searchForInit(full, maxDepth - 1)) return true;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const ext = extname(entry);
|
|
279
|
+
if (![".ts", ".tsx", ".js", ".jsx", ".mjs", ".astro"].includes(ext)) continue;
|
|
280
|
+
const content = readFileOrNull(full);
|
|
281
|
+
if (!content) continue;
|
|
282
|
+
if (content.includes("CometChatUIKit.init") || content.includes("CometChatUIKit.login") || content.includes("@cometchat/chat-uikit-react")) {
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
function classifySource(args) {
|
|
289
|
+
if (args.tokenEndpoint !== null) return "token-endpoint";
|
|
290
|
+
if (args.hasEnv) return "env";
|
|
291
|
+
if (args.hasInferred) return "inferred";
|
|
292
|
+
return "none";
|
|
293
|
+
}
|
|
294
|
+
function detectCredentials(root, envPrefix) {
|
|
295
|
+
const envFlags = detectFromEnv(root, envPrefix);
|
|
296
|
+
const tokenEndpoint = detectTokenEndpoint(root);
|
|
297
|
+
const inferred = detectInferredFromSource(root);
|
|
298
|
+
const source = classifySource({
|
|
299
|
+
hasEnv: envFlags.app_id_present || envFlags.auth_key_present,
|
|
300
|
+
hasInferred: inferred,
|
|
301
|
+
tokenEndpoint
|
|
302
|
+
});
|
|
303
|
+
return {
|
|
304
|
+
source,
|
|
305
|
+
app_id_present: envFlags.app_id_present,
|
|
306
|
+
region_present: envFlags.region_present,
|
|
307
|
+
auth_key_present: envFlags.auth_key_present,
|
|
308
|
+
token_endpoint: tokenEndpoint
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// src/detectors/integration.ts
|
|
313
|
+
import { readdirSync as readdirSync2 } from "node:fs";
|
|
314
|
+
function getInstalledVersion(root) {
|
|
315
|
+
const pkg = readJsonOrNull(p(root, "package.json"));
|
|
316
|
+
if (!pkg) return null;
|
|
317
|
+
const all = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
318
|
+
const version = all["@cometchat/chat-uikit-react"];
|
|
319
|
+
return version ? extractVersion(version) : null;
|
|
320
|
+
}
|
|
321
|
+
function readStateFileOwnedFiles(root) {
|
|
322
|
+
const state = readJsonOrNull(
|
|
323
|
+
p(root, ".cometchat/state.json")
|
|
324
|
+
);
|
|
325
|
+
if (!state || !Array.isArray(state.files_owned)) return null;
|
|
326
|
+
return state.files_owned;
|
|
327
|
+
}
|
|
328
|
+
function findCometchatDirs(root) {
|
|
329
|
+
const found = [];
|
|
330
|
+
const candidates = ["src/cometchat", "app/cometchat", "src/app/cometchat"];
|
|
331
|
+
for (const dir of candidates) {
|
|
332
|
+
const fullPath = p(root, dir);
|
|
333
|
+
if (!pathExists(fullPath)) continue;
|
|
334
|
+
try {
|
|
335
|
+
const entries = readdirSync2(fullPath);
|
|
336
|
+
for (const entry of entries) {
|
|
337
|
+
found.push(`${dir}/${entry}`);
|
|
338
|
+
}
|
|
339
|
+
} catch {
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return found;
|
|
343
|
+
}
|
|
344
|
+
function findIntegrationEntryFiles(root) {
|
|
345
|
+
const checks = [
|
|
346
|
+
"src/cometchat/CometChatNoSSR.tsx",
|
|
347
|
+
"src/app/cometchat/CometChatNoSSR.tsx",
|
|
348
|
+
"app/cometchat/CometChatNoSSR.tsx",
|
|
349
|
+
"src/cometchat/CometChatSelector.tsx",
|
|
350
|
+
"src/components/cometchat/index.tsx"
|
|
351
|
+
];
|
|
352
|
+
const found = [];
|
|
353
|
+
for (const c of checks) {
|
|
354
|
+
if (pathExists(p(root, c))) found.push(c);
|
|
355
|
+
}
|
|
356
|
+
const appTsx = readFileOrNull(p(root, "src/App.tsx"));
|
|
357
|
+
if (appTsx && appTsx.includes("@cometchat/chat-uikit-react")) {
|
|
358
|
+
found.push("src/App.tsx");
|
|
359
|
+
}
|
|
360
|
+
return found;
|
|
361
|
+
}
|
|
362
|
+
function detectExistingIntegration(root) {
|
|
363
|
+
const installedVersion = getInstalledVersion(root);
|
|
364
|
+
const stateFiles = readStateFileOwnedFiles(root);
|
|
365
|
+
if (stateFiles !== null) {
|
|
366
|
+
return {
|
|
367
|
+
installed: true,
|
|
368
|
+
version: installedVersion,
|
|
369
|
+
files: stateFiles
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
const dirs = findCometchatDirs(root);
|
|
373
|
+
const entries = findIntegrationEntryFiles(root);
|
|
374
|
+
const allFiles = [.../* @__PURE__ */ new Set([...dirs, ...entries])];
|
|
375
|
+
return {
|
|
376
|
+
installed: installedVersion !== null || allFiles.length > 0,
|
|
377
|
+
version: installedVersion,
|
|
378
|
+
files: allFiles
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// src/detectors/compatibility.ts
|
|
383
|
+
function computeCompatibility(fw) {
|
|
384
|
+
const warnings = [];
|
|
385
|
+
if (fw.framework === null) {
|
|
386
|
+
return {
|
|
387
|
+
supported: false,
|
|
388
|
+
warnings: [
|
|
389
|
+
"Could not detect a supported React framework. CometChat skills v2 supports: reactjs (Vite/CRA), nextjs, react-router (v6/v7), astro."
|
|
390
|
+
]
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
if (fw.framework === "nextjs") {
|
|
394
|
+
if (fw.router === null) {
|
|
395
|
+
warnings.push(
|
|
396
|
+
"Next.js detected but neither App Router (app/) nor Pages Router (pages/) directory found. Integration may need a router setup first."
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
const major = fw.framework_version ? parseInt(fw.framework_version.split(".")[0] ?? "0", 10) : null;
|
|
400
|
+
if (major !== null && major < 13) {
|
|
401
|
+
warnings.push(
|
|
402
|
+
`Next.js ${fw.framework_version} is older than the tested baseline (13+). Integration may work but is not regression-tested.`
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (fw.framework === "react-router" && fw.router === "v6") {
|
|
407
|
+
warnings.push(
|
|
408
|
+
"React Router v6 detected. v7 is the recommended version for new projects; v6 is supported but receives less active maintenance."
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
if (fw.framework === "reactjs" && fw.bundler === "webpack") {
|
|
412
|
+
warnings.push(
|
|
413
|
+
"Create React App (CRA) detected. CRA is no longer actively maintained; we recommend migrating to Vite when convenient. CometChat integration still works."
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
return { supported: true, warnings };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/detectors/architecture-context.ts
|
|
420
|
+
var AGENT_INSTRUCTION_CANDIDATES = [
|
|
421
|
+
"AGENTS.md",
|
|
422
|
+
// Codex / generic
|
|
423
|
+
"CLAUDE.md",
|
|
424
|
+
// Claude Code
|
|
425
|
+
"GEMINI.md",
|
|
426
|
+
// Gemini CLI
|
|
427
|
+
"CURSOR.md",
|
|
428
|
+
// Cursor
|
|
429
|
+
".cursorrules",
|
|
430
|
+
// Cursor (legacy)
|
|
431
|
+
".cursor/rules",
|
|
432
|
+
// Cursor (newer)
|
|
433
|
+
".github/copilot-instructions.md",
|
|
434
|
+
// GitHub Copilot
|
|
435
|
+
".windsurfrules",
|
|
436
|
+
// Windsurf
|
|
437
|
+
".aider.conf.yml",
|
|
438
|
+
// Aider
|
|
439
|
+
".kiro/agent-instructions.md",
|
|
440
|
+
// Kiro
|
|
441
|
+
"AGENT_INSTRUCTIONS.md"
|
|
442
|
+
// generic / Continue / Cline
|
|
443
|
+
];
|
|
444
|
+
var ARCHITECTURE_DOC_CANDIDATES = [
|
|
445
|
+
"ARCHITECTURE.md",
|
|
446
|
+
"CONTRIBUTING.md",
|
|
447
|
+
"CONVENTIONS.md",
|
|
448
|
+
"docs/architecture/",
|
|
449
|
+
"docs/adr/",
|
|
450
|
+
"docs/architecture-decisions/",
|
|
451
|
+
"docs/decisions/",
|
|
452
|
+
"ADRs/",
|
|
453
|
+
".docs/architecture.md"
|
|
454
|
+
];
|
|
455
|
+
var SKILL_DIR_CANDIDATES = [
|
|
456
|
+
".kiro/skills",
|
|
457
|
+
".claude/skills",
|
|
458
|
+
".cursor/skills",
|
|
459
|
+
".windsurf/skills"
|
|
460
|
+
];
|
|
461
|
+
function detectArchitectureContext(root) {
|
|
462
|
+
const agent_instruction_files = [];
|
|
463
|
+
for (const candidate of AGENT_INSTRUCTION_CANDIDATES) {
|
|
464
|
+
if (pathExists(p(root, candidate))) {
|
|
465
|
+
agent_instruction_files.push(candidate);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const architecture_docs = [];
|
|
469
|
+
for (const candidate of ARCHITECTURE_DOC_CANDIDATES) {
|
|
470
|
+
if (pathExists(p(root, candidate))) {
|
|
471
|
+
architecture_docs.push(candidate);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const installed_skill_directories = [];
|
|
475
|
+
for (const candidate of SKILL_DIR_CANDIDATES) {
|
|
476
|
+
if (pathExists(p(root, candidate))) {
|
|
477
|
+
installed_skill_directories.push(candidate);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return {
|
|
481
|
+
agent_instruction_files,
|
|
482
|
+
architecture_docs,
|
|
483
|
+
installed_skill_directories
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// src/detectors/index.ts
|
|
488
|
+
async function runDetectors(root) {
|
|
489
|
+
const fw = detectFramework(root);
|
|
490
|
+
const package_manager = detectPackageManager(root);
|
|
491
|
+
const credentials = detectCredentials(root, fw.env_prefix);
|
|
492
|
+
const existing_integration = detectExistingIntegration(root);
|
|
493
|
+
const compatibility = computeCompatibility(fw);
|
|
494
|
+
const architecture_context = detectArchitectureContext(root);
|
|
495
|
+
return {
|
|
496
|
+
project_root: root,
|
|
497
|
+
framework: fw.framework,
|
|
498
|
+
framework_version: fw.framework_version,
|
|
499
|
+
router: fw.router,
|
|
500
|
+
bundler: fw.bundler,
|
|
501
|
+
ssr_strategy: fw.ssr_strategy,
|
|
502
|
+
env_prefix: fw.env_prefix,
|
|
503
|
+
package_manager,
|
|
504
|
+
credentials,
|
|
505
|
+
existing_integration,
|
|
506
|
+
compatibility,
|
|
507
|
+
architecture_context
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// src/commands/detect.ts
|
|
512
|
+
function isJsonMode(args) {
|
|
513
|
+
return args.flags.json === true || args.flags.json === "true";
|
|
514
|
+
}
|
|
515
|
+
function projectPath(args) {
|
|
516
|
+
const fromArg = args.positional[0];
|
|
517
|
+
return resolve(fromArg ?? process.cwd());
|
|
518
|
+
}
|
|
519
|
+
async function detect(args) {
|
|
520
|
+
const root = projectPath(args);
|
|
521
|
+
const result = await runDetectors(root);
|
|
522
|
+
if (isJsonMode(args)) {
|
|
523
|
+
console.log(JSON.stringify(result, null, 2));
|
|
524
|
+
return result.compatibility.supported ? 0 : 2;
|
|
525
|
+
}
|
|
526
|
+
printHumanReadable(result);
|
|
527
|
+
return result.compatibility.supported ? 0 : 2;
|
|
528
|
+
}
|
|
529
|
+
function printHumanReadable(r) {
|
|
530
|
+
const lines = [];
|
|
531
|
+
lines.push("");
|
|
532
|
+
lines.push(` Project: ${r.project_root}`);
|
|
533
|
+
lines.push(` Framework: ${r.framework ?? "unknown"}${r.framework_version ? ` (${r.framework_version})` : ""}`);
|
|
534
|
+
if (r.router !== null) lines.push(` Router: ${r.router}`);
|
|
535
|
+
if (r.bundler !== null) lines.push(` Bundler: ${r.bundler}`);
|
|
536
|
+
if (r.ssr_strategy !== null) lines.push(` SSR strategy: ${r.ssr_strategy}`);
|
|
537
|
+
if (r.env_prefix !== null) lines.push(` Env var prefix: ${r.env_prefix}`);
|
|
538
|
+
if (r.package_manager !== null) lines.push(` Package manager: ${r.package_manager}`);
|
|
539
|
+
lines.push("");
|
|
540
|
+
lines.push(" Credentials:");
|
|
541
|
+
lines.push(` Source: ${r.credentials.source}`);
|
|
542
|
+
lines.push(` APP_ID present: ${r.credentials.app_id_present ? "yes" : "no"}`);
|
|
543
|
+
lines.push(` REGION present: ${r.credentials.region_present ? "yes" : "no"}`);
|
|
544
|
+
lines.push(` AUTH_KEY present: ${r.credentials.auth_key_present ? "yes" : "no"}`);
|
|
545
|
+
if (r.credentials.token_endpoint !== null) {
|
|
546
|
+
lines.push(` Token endpoint: ${r.credentials.token_endpoint}`);
|
|
547
|
+
}
|
|
548
|
+
lines.push("");
|
|
549
|
+
lines.push(" Existing CometChat integration:");
|
|
550
|
+
lines.push(` Installed: ${r.existing_integration.installed ? "yes" : "no"}`);
|
|
551
|
+
if (r.existing_integration.version !== null) {
|
|
552
|
+
lines.push(` Version: ${r.existing_integration.version}`);
|
|
553
|
+
}
|
|
554
|
+
if (r.existing_integration.files.length > 0) {
|
|
555
|
+
lines.push(` Files:`);
|
|
556
|
+
for (const f of r.existing_integration.files) {
|
|
557
|
+
lines.push(` - ${f}`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
lines.push("");
|
|
561
|
+
lines.push(` Compatibility: ${r.compatibility.supported ? "supported" : "NOT supported"}`);
|
|
562
|
+
if (r.compatibility.warnings.length > 0) {
|
|
563
|
+
lines.push(" Warnings:");
|
|
564
|
+
for (const w of r.compatibility.warnings) {
|
|
565
|
+
lines.push(` - ${w}`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
lines.push("");
|
|
569
|
+
console.log(lines.join("\n"));
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// src/commands/experiences.ts
|
|
573
|
+
var ALL_EXPERIENCES = [
|
|
574
|
+
{
|
|
575
|
+
id: 1,
|
|
576
|
+
slug: "conversation-list",
|
|
577
|
+
name: "Conversation List + Message View",
|
|
578
|
+
description: "Two-panel layout. Left: list of all conversations. Right: message thread for the selected conversation. Best when users browse and switch between multiple conversations.",
|
|
579
|
+
default_placement: "dedicated-route",
|
|
580
|
+
files_touched: 5,
|
|
581
|
+
components_required: [
|
|
582
|
+
"CometChatConversations",
|
|
583
|
+
"CometChatMessageHeader",
|
|
584
|
+
"CometChatMessageList",
|
|
585
|
+
"CometChatMessageComposer"
|
|
586
|
+
],
|
|
587
|
+
supported_frameworks: ["reactjs", "nextjs", "react-router", "astro"]
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
id: 2,
|
|
591
|
+
slug: "one-to-one",
|
|
592
|
+
name: "One-to-One Chat",
|
|
593
|
+
description: "Single message window with a hardcoded user or group. No conversation list. Best for customer support widgets or matched-pair chat.",
|
|
594
|
+
default_placement: "dedicated-route",
|
|
595
|
+
files_touched: 4,
|
|
596
|
+
components_required: [
|
|
597
|
+
"CometChatMessageHeader",
|
|
598
|
+
"CometChatMessageList",
|
|
599
|
+
"CometChatMessageComposer"
|
|
600
|
+
],
|
|
601
|
+
supported_frameworks: ["reactjs", "nextjs", "react-router", "astro"]
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
id: 3,
|
|
605
|
+
slug: "tab-based",
|
|
606
|
+
name: "Tab-Based Chat",
|
|
607
|
+
description: "Left panel has a tab bar (Chats / Calls / Users / Groups). Each tab shows the corresponding CometChat list. Right panel shows messages for selected conversation. Best for full-featured messenger-style apps.",
|
|
608
|
+
default_placement: "dedicated-route",
|
|
609
|
+
files_touched: 7,
|
|
610
|
+
components_required: [
|
|
611
|
+
"CometChatConversations",
|
|
612
|
+
"CometChatUsers",
|
|
613
|
+
"CometChatGroups",
|
|
614
|
+
"CometChatCallLogs",
|
|
615
|
+
"CometChatMessageHeader",
|
|
616
|
+
"CometChatMessageList",
|
|
617
|
+
"CometChatMessageComposer"
|
|
618
|
+
],
|
|
619
|
+
supported_frameworks: ["reactjs", "nextjs", "react-router", "astro"]
|
|
620
|
+
}
|
|
621
|
+
];
|
|
622
|
+
function isJsonMode2(args) {
|
|
623
|
+
return args.flags.json === true || args.flags.json === "true";
|
|
624
|
+
}
|
|
625
|
+
function frameworkFilter(args) {
|
|
626
|
+
const f = args.flags.framework;
|
|
627
|
+
if (typeof f !== "string") return null;
|
|
628
|
+
if (f === "reactjs" || f === "nextjs" || f === "react-router" || f === "astro") {
|
|
629
|
+
return f;
|
|
630
|
+
}
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
async function experiences(args) {
|
|
634
|
+
const filter = frameworkFilter(args);
|
|
635
|
+
const list = filter ? ALL_EXPERIENCES.filter((e) => e.supported_frameworks.includes(filter)) : ALL_EXPERIENCES;
|
|
636
|
+
if (isJsonMode2(args)) {
|
|
637
|
+
console.log(JSON.stringify({ experiences: list }, null, 2));
|
|
638
|
+
return 0;
|
|
639
|
+
}
|
|
640
|
+
console.log("");
|
|
641
|
+
console.log(" Available chat experiences:");
|
|
642
|
+
console.log("");
|
|
643
|
+
for (const e of list) {
|
|
644
|
+
console.log(` [${e.id}] ${e.name} (slug: ${e.slug})`);
|
|
645
|
+
console.log(` ${e.description}`);
|
|
646
|
+
console.log(
|
|
647
|
+
` Default placement: ${e.default_placement} \u2022 Files touched: ${e.files_touched}`
|
|
648
|
+
);
|
|
649
|
+
console.log(` Components: ${e.components_required.join(", ")}`);
|
|
650
|
+
console.log(
|
|
651
|
+
` Frameworks: ${e.supported_frameworks.join(", ")}`
|
|
652
|
+
);
|
|
653
|
+
console.log("");
|
|
654
|
+
}
|
|
655
|
+
return 0;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// src/commands/view.ts
|
|
659
|
+
import { resolve as resolve3 } from "node:path";
|
|
660
|
+
|
|
661
|
+
// src/utils/registry.ts
|
|
662
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
663
|
+
import { dirname, join as join3, resolve as resolve2 } from "node:path";
|
|
664
|
+
import { fileURLToPath } from "node:url";
|
|
665
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
666
|
+
var __dirname = dirname(__filename);
|
|
667
|
+
function findRegistryRoot() {
|
|
668
|
+
const candidates = [
|
|
669
|
+
// (1) Published install: registry bundled into the CLI's dist/
|
|
670
|
+
// bin/cometchat.mjs imports dist/index.js → __dirname is .../dist
|
|
671
|
+
// so dist/registry/ sits next to dist/index.js
|
|
672
|
+
resolve2(__dirname, "registry"),
|
|
673
|
+
// (2) Bundled-dev: same shape as published but inside packages/cli/dist
|
|
674
|
+
resolve2(__dirname, "../registry"),
|
|
675
|
+
// (3) Dev mode: cli/src/utils → cli → packages → registry
|
|
676
|
+
resolve2(__dirname, "../../../registry"),
|
|
677
|
+
// (4) Dev mode bundled: cli/dist → cli → packages → registry
|
|
678
|
+
resolve2(__dirname, "../../registry")
|
|
679
|
+
];
|
|
680
|
+
for (const c of candidates) {
|
|
681
|
+
const sentinel = join3(c, "v6", "experiences", "1-conversation-list", "reactjs.template.json");
|
|
682
|
+
if (existsSync2(sentinel)) {
|
|
683
|
+
return c;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
throw new Error(
|
|
687
|
+
"Could not locate @cometchat/skills-registry. Searched: " + candidates.join(", ")
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
var ROOT = findRegistryRoot();
|
|
691
|
+
function experienceDir(experienceId) {
|
|
692
|
+
switch (experienceId) {
|
|
693
|
+
case 1:
|
|
694
|
+
return "1-conversation-list";
|
|
695
|
+
case 2:
|
|
696
|
+
return "2-one-to-one";
|
|
697
|
+
case 3:
|
|
698
|
+
return "3-tab-based";
|
|
699
|
+
default:
|
|
700
|
+
throw new Error(`Unknown experience id: ${experienceId}`);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
function loadManifest(sdkMajor, framework, experienceId) {
|
|
704
|
+
const path = join3(
|
|
705
|
+
ROOT,
|
|
706
|
+
`v${sdkMajor}`,
|
|
707
|
+
"experiences",
|
|
708
|
+
experienceDir(experienceId),
|
|
709
|
+
`${framework}.template.json`
|
|
710
|
+
);
|
|
711
|
+
let content;
|
|
712
|
+
try {
|
|
713
|
+
content = readFileSync2(path, "utf8");
|
|
714
|
+
} catch {
|
|
715
|
+
throw new Error(
|
|
716
|
+
`No template manifest for framework=${framework} experience=${experienceId} sdkMajor=${sdkMajor} (looked at ${path})`
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
return JSON.parse(content);
|
|
720
|
+
}
|
|
721
|
+
function readTemplateFile(sdkMajor, framework, experienceId, templateRef) {
|
|
722
|
+
const path = join3(
|
|
723
|
+
ROOT,
|
|
724
|
+
`v${sdkMajor}`,
|
|
725
|
+
"experiences",
|
|
726
|
+
experienceDir(experienceId),
|
|
727
|
+
"templates",
|
|
728
|
+
framework,
|
|
729
|
+
templateRef
|
|
730
|
+
);
|
|
731
|
+
return readFileSync2(path, "utf8");
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// src/utils/template.ts
|
|
735
|
+
function substitute(content, subs) {
|
|
736
|
+
return content.replace(/\{\{([A-Z_][A-Z0-9_]*)\}\}/g, (match, varName) => {
|
|
737
|
+
if (varName in subs) return subs[varName];
|
|
738
|
+
return match;
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
function defaultSubs(envPrefix) {
|
|
742
|
+
return {
|
|
743
|
+
ENV_PREFIX: envPrefix,
|
|
744
|
+
LOGIN_UID: "cometchat-uid-1",
|
|
745
|
+
CHAT_UID: "cometchat-uid-2"
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// src/commands/view.ts
|
|
750
|
+
function isJsonMode3(args) {
|
|
751
|
+
return args.flags.json === true || args.flags.json === "true";
|
|
752
|
+
}
|
|
753
|
+
function projectPath2(args) {
|
|
754
|
+
const fromFlag = args.flags.path;
|
|
755
|
+
if (typeof fromFlag === "string") return resolve3(fromFlag);
|
|
756
|
+
return resolve3(process.cwd());
|
|
757
|
+
}
|
|
758
|
+
function getNumberFlag(args, name) {
|
|
759
|
+
const v = args.flags[name];
|
|
760
|
+
if (typeof v !== "string") return null;
|
|
761
|
+
const n = parseInt(v, 10);
|
|
762
|
+
return Number.isFinite(n) ? n : null;
|
|
763
|
+
}
|
|
764
|
+
function getStringFlag(args, name) {
|
|
765
|
+
const v = args.flags[name];
|
|
766
|
+
return typeof v === "string" ? v : null;
|
|
767
|
+
}
|
|
768
|
+
function resolveExperience(experienceArg) {
|
|
769
|
+
if (experienceArg === null) return null;
|
|
770
|
+
if (experienceArg === "1" || experienceArg === "conversation-list") return 1;
|
|
771
|
+
if (experienceArg === "2" || experienceArg === "one-to-one") return 2;
|
|
772
|
+
if (experienceArg === "3" || experienceArg === "tab-based") return 3;
|
|
773
|
+
const n = parseInt(experienceArg, 10);
|
|
774
|
+
if (n >= 1 && n <= 3) return n;
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
function buildFileOutputs(manifest, subs, sdkMajor) {
|
|
778
|
+
const out = [];
|
|
779
|
+
for (const f of manifest.files) {
|
|
780
|
+
if (f.action === "create") {
|
|
781
|
+
const raw = readTemplateFile(sdkMajor, manifest.framework, manifest.experience_id, f.template_ref);
|
|
782
|
+
const content = substitute(raw, subs);
|
|
783
|
+
const path = substitute(f.path, subs);
|
|
784
|
+
out.push({
|
|
785
|
+
path,
|
|
786
|
+
action: "create",
|
|
787
|
+
content,
|
|
788
|
+
owned: f.owned ?? true
|
|
789
|
+
});
|
|
790
|
+
} else if (f.action === "patch") {
|
|
791
|
+
const path = substitute(f.path, subs);
|
|
792
|
+
const patchContent = f.patch_content ? substitute(f.patch_content, subs) : "";
|
|
793
|
+
out.push({
|
|
794
|
+
path,
|
|
795
|
+
action: "patch",
|
|
796
|
+
patch_content: patchContent,
|
|
797
|
+
patch_mode: f.patch_mode ?? "prepend",
|
|
798
|
+
owned: false
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
return out;
|
|
803
|
+
}
|
|
804
|
+
async function view(args) {
|
|
805
|
+
const experienceArg = getStringFlag(args, "experience");
|
|
806
|
+
const frameworkArg = getStringFlag(args, "framework");
|
|
807
|
+
const sdkMajor = getNumberFlag(args, "sdk-major") ?? 6;
|
|
808
|
+
const experienceId = resolveExperience(experienceArg);
|
|
809
|
+
if (experienceId === null) {
|
|
810
|
+
console.error("Error: --experience is required (one of: 1, 2, 3, conversation-list, one-to-one, tab-based)");
|
|
811
|
+
return 1;
|
|
812
|
+
}
|
|
813
|
+
let framework = frameworkArg;
|
|
814
|
+
let envPrefix = null;
|
|
815
|
+
const root = projectPath2(args);
|
|
816
|
+
if (!framework) {
|
|
817
|
+
const detected = await runDetectors(root);
|
|
818
|
+
if (detected.framework === null) {
|
|
819
|
+
console.error("Error: --framework not specified and no React framework detected in " + root);
|
|
820
|
+
console.error("Pass --framework reactjs|nextjs|react-router|astro");
|
|
821
|
+
return 1;
|
|
822
|
+
}
|
|
823
|
+
framework = detected.framework;
|
|
824
|
+
envPrefix = detected.env_prefix;
|
|
825
|
+
}
|
|
826
|
+
if (!envPrefix) {
|
|
827
|
+
envPrefix = framework === "nextjs" ? "NEXT_PUBLIC_" : framework === "astro" ? "PUBLIC_" : "VITE_";
|
|
828
|
+
}
|
|
829
|
+
let manifest;
|
|
830
|
+
try {
|
|
831
|
+
manifest = loadManifest(sdkMajor, framework, experienceId);
|
|
832
|
+
} catch (err) {
|
|
833
|
+
console.error("Error: " + (err instanceof Error ? err.message : String(err)));
|
|
834
|
+
return 1;
|
|
835
|
+
}
|
|
836
|
+
const subs = defaultSubs(envPrefix);
|
|
837
|
+
const files = buildFileOutputs(manifest, subs, sdkMajor);
|
|
838
|
+
const envVarsToAdd = manifest.env_vars.map((v) => ({
|
|
839
|
+
name: substitute(v.name, subs),
|
|
840
|
+
required: v.required,
|
|
841
|
+
secret: v.secret ?? false
|
|
842
|
+
}));
|
|
843
|
+
const result = {
|
|
844
|
+
template_version: manifest.version,
|
|
845
|
+
framework: manifest.framework,
|
|
846
|
+
experience_id: manifest.experience_id,
|
|
847
|
+
files,
|
|
848
|
+
deps_to_install: manifest.deps.map((d) => ({
|
|
849
|
+
name: d.name,
|
|
850
|
+
version: d.version,
|
|
851
|
+
dev: d.dev ?? false
|
|
852
|
+
})),
|
|
853
|
+
env_vars_to_add: envVarsToAdd,
|
|
854
|
+
next_steps: (manifest.next_steps ?? []).map((s) => substitute(s, subs)),
|
|
855
|
+
warnings: []
|
|
856
|
+
};
|
|
857
|
+
if (isJsonMode3(args)) {
|
|
858
|
+
console.log(JSON.stringify(result, null, 2));
|
|
859
|
+
return 0;
|
|
860
|
+
}
|
|
861
|
+
printHumanReadable2(result);
|
|
862
|
+
return 0;
|
|
863
|
+
}
|
|
864
|
+
function printHumanReadable2(r) {
|
|
865
|
+
const lines = [];
|
|
866
|
+
lines.push("");
|
|
867
|
+
lines.push(` Preview: experience ${r.experience_id} on ${r.framework}`);
|
|
868
|
+
lines.push(` Template version: ${r.template_version}`);
|
|
869
|
+
lines.push("");
|
|
870
|
+
lines.push(" Dependencies to install:");
|
|
871
|
+
for (const d of r.deps_to_install) {
|
|
872
|
+
const tag = d.dev ? " (dev)" : "";
|
|
873
|
+
lines.push(` + ${d.name}@${d.version}${tag}`);
|
|
874
|
+
}
|
|
875
|
+
lines.push("");
|
|
876
|
+
lines.push(" Environment variables to add:");
|
|
877
|
+
for (const e of r.env_vars_to_add) {
|
|
878
|
+
const tag = e.secret ? " (secret)" : "";
|
|
879
|
+
lines.push(` + ${e.name}${tag}`);
|
|
880
|
+
}
|
|
881
|
+
lines.push("");
|
|
882
|
+
lines.push(" Files to create/patch:");
|
|
883
|
+
for (const f of r.files) {
|
|
884
|
+
const action = f.action === "create" ? "create" : `patch (${f.patch_mode})`;
|
|
885
|
+
const owned = f.owned ? " [owned]" : "";
|
|
886
|
+
lines.push(` ${action}: ${f.path}${owned}`);
|
|
887
|
+
}
|
|
888
|
+
if (r.next_steps.length > 0) {
|
|
889
|
+
lines.push("");
|
|
890
|
+
lines.push(" Next steps after apply:");
|
|
891
|
+
for (const step of r.next_steps) {
|
|
892
|
+
lines.push(` - ${step}`);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
lines.push("");
|
|
896
|
+
console.log(lines.join("\n"));
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// src/commands/apply.ts
|
|
900
|
+
import { resolve as resolve4 } from "node:path";
|
|
901
|
+
import { dirname as posixDirname, join as posixJoin, relative as posixRelative } from "node:path/posix";
|
|
902
|
+
|
|
903
|
+
// src/utils/audit-log.ts
|
|
904
|
+
import { mkdirSync, readFileSync as readFileSync3, writeFileSync } from "node:fs";
|
|
905
|
+
import { dirname as dirname2, join as join4 } from "node:path";
|
|
906
|
+
var AUDIT_LOG_PATH = ".cometchat/AUDIT_LOG.md";
|
|
907
|
+
function appendAuditEntry(projectRoot, entry) {
|
|
908
|
+
const filePath = join4(projectRoot, AUDIT_LOG_PATH);
|
|
909
|
+
mkdirSync(dirname2(filePath), { recursive: true });
|
|
910
|
+
let body = "";
|
|
911
|
+
if (!pathExists(filePath)) {
|
|
912
|
+
body = `# CometChat Integration Audit Log
|
|
913
|
+
|
|
914
|
+
> Auto-generated by the \`cometchat\` CLI. Append-only \u2014 every command
|
|
915
|
+
> writes one section here so a human reader can answer "why is this
|
|
916
|
+
> integration set up this way?" without reverse-engineering state.json.
|
|
917
|
+
>
|
|
918
|
+
> The machine-readable source of truth lives in \`.cometchat/state.json\`.
|
|
919
|
+
> This file is for humans.
|
|
920
|
+
|
|
921
|
+
`;
|
|
922
|
+
} else {
|
|
923
|
+
try {
|
|
924
|
+
body = readFileSync3(filePath, "utf8");
|
|
925
|
+
} catch {
|
|
926
|
+
body = "";
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
body += renderEntry(entry);
|
|
930
|
+
writeFileSync(filePath, body, "utf8");
|
|
931
|
+
}
|
|
932
|
+
function renderEntry(entry) {
|
|
933
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
934
|
+
const lines = [];
|
|
935
|
+
lines.push(`---
|
|
936
|
+
`);
|
|
937
|
+
lines.push(`## ${ts} \u2014 \`cometchat ${entry.command}\``);
|
|
938
|
+
lines.push("");
|
|
939
|
+
lines.push(`**${entry.summary}**`);
|
|
940
|
+
lines.push("");
|
|
941
|
+
if (entry.inputs && Object.keys(entry.inputs).length > 0) {
|
|
942
|
+
lines.push(`### Inputs`);
|
|
943
|
+
lines.push("");
|
|
944
|
+
for (const [key, value] of Object.entries(entry.inputs)) {
|
|
945
|
+
lines.push(`- **${key}:** \`${value}\``);
|
|
946
|
+
}
|
|
947
|
+
lines.push("");
|
|
948
|
+
}
|
|
949
|
+
if (entry.decisions && Object.keys(entry.decisions).length > 0) {
|
|
950
|
+
lines.push(`### Decisions & rationale`);
|
|
951
|
+
lines.push("");
|
|
952
|
+
for (const [decision, rationale] of Object.entries(entry.decisions)) {
|
|
953
|
+
lines.push(`- **${decision}** \u2014 ${rationale}`);
|
|
954
|
+
}
|
|
955
|
+
lines.push("");
|
|
956
|
+
}
|
|
957
|
+
if (entry.files_created && entry.files_created.length > 0) {
|
|
958
|
+
lines.push(`### Files created (${entry.files_created.length})`);
|
|
959
|
+
lines.push("");
|
|
960
|
+
for (const f of entry.files_created) lines.push(`- \`${f}\``);
|
|
961
|
+
lines.push("");
|
|
962
|
+
}
|
|
963
|
+
if (entry.files_patched && entry.files_patched.length > 0) {
|
|
964
|
+
lines.push(`### Files patched (${entry.files_patched.length})`);
|
|
965
|
+
lines.push("");
|
|
966
|
+
for (const f of entry.files_patched) lines.push(`- \`${f}\``);
|
|
967
|
+
lines.push("");
|
|
968
|
+
}
|
|
969
|
+
if (entry.files_modified && entry.files_modified.length > 0) {
|
|
970
|
+
lines.push(`### Files modified in place (${entry.files_modified.length})`);
|
|
971
|
+
lines.push("");
|
|
972
|
+
for (const f of entry.files_modified) lines.push(`- \`${f}\``);
|
|
973
|
+
lines.push("");
|
|
974
|
+
}
|
|
975
|
+
if (entry.files_skipped && entry.files_skipped.length > 0) {
|
|
976
|
+
lines.push(`### Files skipped (${entry.files_skipped.length})`);
|
|
977
|
+
lines.push("");
|
|
978
|
+
for (const f of entry.files_skipped) lines.push(`- \`${f}\``);
|
|
979
|
+
lines.push("");
|
|
980
|
+
}
|
|
981
|
+
if (entry.next_actions && entry.next_actions.length > 0) {
|
|
982
|
+
lines.push(`### Manual next actions`);
|
|
983
|
+
lines.push("");
|
|
984
|
+
for (const a of entry.next_actions) lines.push(`- ${a}`);
|
|
985
|
+
lines.push("");
|
|
986
|
+
}
|
|
987
|
+
return lines.join("\n");
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// src/commands/apply.ts
|
|
991
|
+
import { spawnSync } from "node:child_process";
|
|
992
|
+
|
|
993
|
+
// src/utils/writer.ts
|
|
994
|
+
import { copyFileSync, mkdirSync as mkdirSync3, readFileSync as readFileSync5, renameSync, rmSync, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
995
|
+
import { dirname as dirname4, join as join6, relative } from "node:path";
|
|
996
|
+
|
|
997
|
+
// src/state.ts
|
|
998
|
+
import { createHash } from "node:crypto";
|
|
999
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1000
|
+
import { dirname as dirname3, join as join5 } from "node:path";
|
|
1001
|
+
var STATE_SCHEMA_VERSION = 1;
|
|
1002
|
+
var STATE_FILE_PATH = ".cometchat/state.json";
|
|
1003
|
+
function sha256(content) {
|
|
1004
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
1005
|
+
}
|
|
1006
|
+
function readState(projectRoot) {
|
|
1007
|
+
const state = readJsonOrNull(p(projectRoot, STATE_FILE_PATH));
|
|
1008
|
+
if (!state) return null;
|
|
1009
|
+
if (state.schema_version !== STATE_SCHEMA_VERSION) {
|
|
1010
|
+
return null;
|
|
1011
|
+
}
|
|
1012
|
+
return state;
|
|
1013
|
+
}
|
|
1014
|
+
function writeState(projectRoot, state) {
|
|
1015
|
+
const filePath = join5(projectRoot, STATE_FILE_PATH);
|
|
1016
|
+
mkdirSync2(dirname3(filePath), { recursive: true });
|
|
1017
|
+
writeFileSync2(filePath, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
1018
|
+
}
|
|
1019
|
+
function hasState(projectRoot) {
|
|
1020
|
+
return pathExists(p(projectRoot, STATE_FILE_PATH));
|
|
1021
|
+
}
|
|
1022
|
+
function detectDrift(projectRoot, state) {
|
|
1023
|
+
const modified = [];
|
|
1024
|
+
const missing = [];
|
|
1025
|
+
const unchanged = [];
|
|
1026
|
+
for (const file of state.files_owned) {
|
|
1027
|
+
const fullPath = join5(projectRoot, file);
|
|
1028
|
+
if (!pathExists(fullPath)) {
|
|
1029
|
+
missing.push(file);
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1032
|
+
let content;
|
|
1033
|
+
try {
|
|
1034
|
+
content = readFileSync4(fullPath, "utf8");
|
|
1035
|
+
} catch {
|
|
1036
|
+
missing.push(file);
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
const currentHash = sha256(content);
|
|
1040
|
+
const expectedHash = state.checksums[file];
|
|
1041
|
+
if (expectedHash === void 0) {
|
|
1042
|
+
modified.push(file);
|
|
1043
|
+
continue;
|
|
1044
|
+
}
|
|
1045
|
+
if (currentHash === expectedHash) {
|
|
1046
|
+
unchanged.push(file);
|
|
1047
|
+
} else {
|
|
1048
|
+
modified.push(file);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return {
|
|
1052
|
+
modified_files: modified,
|
|
1053
|
+
missing_files: missing,
|
|
1054
|
+
unchanged_files: unchanged
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
function isSameIntegration(state, args) {
|
|
1058
|
+
return state.experience === args.experience && state.framework === args.framework && state.placement === args.placement;
|
|
1059
|
+
}
|
|
1060
|
+
function buildState(args) {
|
|
1061
|
+
return {
|
|
1062
|
+
schema_version: STATE_SCHEMA_VERSION,
|
|
1063
|
+
applied_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1064
|
+
...args
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// src/utils/writer.ts
|
|
1069
|
+
var STAGING_DIR = ".cometchat-staging";
|
|
1070
|
+
var BACKUPS_DIR = ".cometchat-staging/.backups";
|
|
1071
|
+
function applyWrites(projectRoot, ops) {
|
|
1072
|
+
const stagingRoot = join6(projectRoot, STAGING_DIR);
|
|
1073
|
+
const backupsRoot = join6(projectRoot, BACKUPS_DIR);
|
|
1074
|
+
const result = {
|
|
1075
|
+
files_created: [],
|
|
1076
|
+
files_modified: [],
|
|
1077
|
+
files_skipped: [],
|
|
1078
|
+
checksums: {}
|
|
1079
|
+
};
|
|
1080
|
+
const committedTargets = [];
|
|
1081
|
+
try {
|
|
1082
|
+
cleanDir(stagingRoot);
|
|
1083
|
+
cleanDir(backupsRoot);
|
|
1084
|
+
const stagedOps = stageOps(projectRoot, stagingRoot, ops, result);
|
|
1085
|
+
for (const staged of stagedOps) {
|
|
1086
|
+
const targetPath = join6(projectRoot, staged.relPath);
|
|
1087
|
+
mkdirSync3(dirname4(targetPath), { recursive: true });
|
|
1088
|
+
let backupPath = null;
|
|
1089
|
+
if (pathExists(targetPath)) {
|
|
1090
|
+
backupPath = join6(backupsRoot, staged.relPath);
|
|
1091
|
+
mkdirSync3(dirname4(backupPath), { recursive: true });
|
|
1092
|
+
copyFileSync(targetPath, backupPath);
|
|
1093
|
+
}
|
|
1094
|
+
renameSync(staged.stagingPath, targetPath);
|
|
1095
|
+
committedTargets.push({ path: targetPath, backup: backupPath });
|
|
1096
|
+
}
|
|
1097
|
+
safeRmRecursive(stagingRoot);
|
|
1098
|
+
return result;
|
|
1099
|
+
} catch (err) {
|
|
1100
|
+
for (const c of committedTargets) {
|
|
1101
|
+
if (c.backup) {
|
|
1102
|
+
try {
|
|
1103
|
+
copyFileSync(c.backup, c.path);
|
|
1104
|
+
} catch {
|
|
1105
|
+
}
|
|
1106
|
+
} else {
|
|
1107
|
+
try {
|
|
1108
|
+
rmSync(c.path);
|
|
1109
|
+
} catch {
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
safeRmRecursive(stagingRoot);
|
|
1114
|
+
throw err;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
function stageOps(projectRoot, stagingRoot, ops, result) {
|
|
1118
|
+
const staged = [];
|
|
1119
|
+
const patchedInMemory = /* @__PURE__ */ new Map();
|
|
1120
|
+
const alreadyRecorded = /* @__PURE__ */ new Set();
|
|
1121
|
+
for (const op of ops) {
|
|
1122
|
+
const targetPath = join6(projectRoot, op.path);
|
|
1123
|
+
const stagingPath = join6(stagingRoot, op.path);
|
|
1124
|
+
if (op.type === "create") {
|
|
1125
|
+
mkdirSync3(dirname4(stagingPath), { recursive: true });
|
|
1126
|
+
writeFileSync3(stagingPath, op.content, "utf8");
|
|
1127
|
+
const isOverwrite = pathExists(targetPath);
|
|
1128
|
+
if (isOverwrite) {
|
|
1129
|
+
if (!alreadyRecorded.has(op.path)) {
|
|
1130
|
+
result.files_modified.push(op.path);
|
|
1131
|
+
alreadyRecorded.add(op.path);
|
|
1132
|
+
}
|
|
1133
|
+
} else {
|
|
1134
|
+
if (!alreadyRecorded.has(op.path)) {
|
|
1135
|
+
result.files_created.push(op.path);
|
|
1136
|
+
alreadyRecorded.add(op.path);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
if (op.owned) {
|
|
1140
|
+
result.checksums[op.path] = sha256(op.content);
|
|
1141
|
+
}
|
|
1142
|
+
patchedInMemory.set(op.path, op.content);
|
|
1143
|
+
const existingStagedIdx2 = staged.findIndex((s) => s.relPath === op.path);
|
|
1144
|
+
if (existingStagedIdx2 !== -1) {
|
|
1145
|
+
staged[existingStagedIdx2] = { relPath: op.path, stagingPath, isCreate: !isOverwrite };
|
|
1146
|
+
} else {
|
|
1147
|
+
staged.push({ relPath: op.path, stagingPath, isCreate: !isOverwrite });
|
|
1148
|
+
}
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
if (!pathExists(targetPath) && !patchedInMemory.has(op.path)) {
|
|
1152
|
+
result.files_skipped.push({
|
|
1153
|
+
path: op.path,
|
|
1154
|
+
reason: `target file does not exist (cannot patch)`
|
|
1155
|
+
});
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
const existingContent = patchedInMemory.get(op.path) ?? readFileSync5(targetPath, "utf8");
|
|
1159
|
+
if (op.skip_if_exists && existingContent.includes(op.skip_if_exists)) {
|
|
1160
|
+
result.files_skipped.push({
|
|
1161
|
+
path: op.path,
|
|
1162
|
+
reason: `already contains: ${op.skip_if_exists}`
|
|
1163
|
+
});
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
const patched = applyPatch(existingContent, op);
|
|
1167
|
+
if (patched === existingContent) {
|
|
1168
|
+
result.files_skipped.push({
|
|
1169
|
+
path: op.path,
|
|
1170
|
+
reason: "patch produced no change"
|
|
1171
|
+
});
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
patchedInMemory.set(op.path, patched);
|
|
1175
|
+
mkdirSync3(dirname4(stagingPath), { recursive: true });
|
|
1176
|
+
writeFileSync3(stagingPath, patched, "utf8");
|
|
1177
|
+
if (!alreadyRecorded.has(op.path)) {
|
|
1178
|
+
result.files_modified.push(op.path);
|
|
1179
|
+
alreadyRecorded.add(op.path);
|
|
1180
|
+
}
|
|
1181
|
+
const existingStagedIdx = staged.findIndex((s) => s.relPath === op.path);
|
|
1182
|
+
if (existingStagedIdx !== -1) {
|
|
1183
|
+
staged[existingStagedIdx] = { relPath: op.path, stagingPath, isCreate: false };
|
|
1184
|
+
} else {
|
|
1185
|
+
staged.push({ relPath: op.path, stagingPath, isCreate: false });
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
return staged;
|
|
1189
|
+
}
|
|
1190
|
+
function applyPatch(existing, op) {
|
|
1191
|
+
switch (op.patch_mode) {
|
|
1192
|
+
case "prepend":
|
|
1193
|
+
return op.patch_content + existing;
|
|
1194
|
+
case "append":
|
|
1195
|
+
return existing + op.patch_content;
|
|
1196
|
+
case "insert-after": {
|
|
1197
|
+
if (!op.anchor) return existing;
|
|
1198
|
+
const idx = existing.indexOf(op.anchor);
|
|
1199
|
+
if (idx === -1) return existing;
|
|
1200
|
+
const insertAt = idx + op.anchor.length;
|
|
1201
|
+
return existing.slice(0, insertAt) + op.patch_content + existing.slice(insertAt);
|
|
1202
|
+
}
|
|
1203
|
+
case "insert-before": {
|
|
1204
|
+
if (!op.anchor) return existing;
|
|
1205
|
+
const idx = existing.indexOf(op.anchor);
|
|
1206
|
+
if (idx === -1) return existing;
|
|
1207
|
+
return existing.slice(0, idx) + op.patch_content + existing.slice(idx);
|
|
1208
|
+
}
|
|
1209
|
+
case "replace-section": {
|
|
1210
|
+
if (!op.anchor) return existing;
|
|
1211
|
+
const idx = existing.indexOf(op.anchor);
|
|
1212
|
+
if (idx === -1) return existing;
|
|
1213
|
+
const lineEnd = existing.indexOf("\n", idx);
|
|
1214
|
+
const replaceEnd = lineEnd === -1 ? existing.length : lineEnd;
|
|
1215
|
+
return existing.slice(0, idx) + op.patch_content + existing.slice(replaceEnd);
|
|
1216
|
+
}
|
|
1217
|
+
default:
|
|
1218
|
+
return existing;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
function cleanDir(dir) {
|
|
1222
|
+
safeRmRecursive(dir);
|
|
1223
|
+
mkdirSync3(dir, { recursive: true });
|
|
1224
|
+
}
|
|
1225
|
+
function safeRmRecursive(dir) {
|
|
1226
|
+
try {
|
|
1227
|
+
if (statSync2(dir).isDirectory()) {
|
|
1228
|
+
rmSync(dir, { recursive: true, force: true });
|
|
1229
|
+
}
|
|
1230
|
+
} catch {
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// src/utils/boilerplate.ts
|
|
1235
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
1236
|
+
var VITE_REACT_TS_APP_MARKERS = [
|
|
1237
|
+
"Vite + React",
|
|
1238
|
+
"vite.svg",
|
|
1239
|
+
"react.svg",
|
|
1240
|
+
"count is",
|
|
1241
|
+
"function App()"
|
|
1242
|
+
];
|
|
1243
|
+
var VITE_MAIN_MARKERS = [
|
|
1244
|
+
"ReactDOM.createRoot",
|
|
1245
|
+
"createRoot",
|
|
1246
|
+
"<StrictMode>",
|
|
1247
|
+
"import App from"
|
|
1248
|
+
];
|
|
1249
|
+
var VITE_APP_CSS_MARKERS = [
|
|
1250
|
+
".counter",
|
|
1251
|
+
".hero",
|
|
1252
|
+
".framework",
|
|
1253
|
+
".vite",
|
|
1254
|
+
// Legacy 2023-era Vite template
|
|
1255
|
+
".logo",
|
|
1256
|
+
".card",
|
|
1257
|
+
".read-the-docs"
|
|
1258
|
+
];
|
|
1259
|
+
var NEXTJS_PAGE_MARKERS = [
|
|
1260
|
+
"page.module.css",
|
|
1261
|
+
"next/image",
|
|
1262
|
+
"Get started by editing",
|
|
1263
|
+
"Vercel logomark"
|
|
1264
|
+
];
|
|
1265
|
+
var ASTRO_INDEX_MARKERS = [
|
|
1266
|
+
"Astro.generator",
|
|
1267
|
+
"<title>Astro</title>"
|
|
1268
|
+
];
|
|
1269
|
+
var REACT_ROUTER_HOME_MARKERS = [
|
|
1270
|
+
"react-router/dev",
|
|
1271
|
+
"What's next?",
|
|
1272
|
+
"Welcome to React Router"
|
|
1273
|
+
];
|
|
1274
|
+
function looksLikeDefault(content, markers) {
|
|
1275
|
+
const found = markers.filter((m) => content.includes(m)).length;
|
|
1276
|
+
return found >= 2;
|
|
1277
|
+
}
|
|
1278
|
+
function readFileSafe(path) {
|
|
1279
|
+
try {
|
|
1280
|
+
if (!pathExists(path)) return null;
|
|
1281
|
+
return readFileSync6(path, "utf8");
|
|
1282
|
+
} catch {
|
|
1283
|
+
return null;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
function checkOverwriteSafety(root, framework, filePath) {
|
|
1287
|
+
const fullPath = p(root, filePath);
|
|
1288
|
+
if (!pathExists(fullPath)) {
|
|
1289
|
+
return {
|
|
1290
|
+
path: filePath,
|
|
1291
|
+
exists: false,
|
|
1292
|
+
is_boilerplate: true,
|
|
1293
|
+
// doesn't exist => "safe to create"
|
|
1294
|
+
current_size: 0,
|
|
1295
|
+
reason: "file does not exist"
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
const content = readFileSafe(fullPath) ?? "";
|
|
1299
|
+
const size = content.length;
|
|
1300
|
+
let markers = null;
|
|
1301
|
+
if (framework === "reactjs") {
|
|
1302
|
+
if (filePath.endsWith("/App.tsx") || filePath.endsWith("/App.jsx")) {
|
|
1303
|
+
markers = VITE_REACT_TS_APP_MARKERS;
|
|
1304
|
+
} else if (filePath.endsWith("/main.tsx") || filePath.endsWith("/main.jsx")) {
|
|
1305
|
+
markers = VITE_MAIN_MARKERS;
|
|
1306
|
+
} else if (filePath.endsWith("/App.css")) {
|
|
1307
|
+
markers = VITE_APP_CSS_MARKERS;
|
|
1308
|
+
}
|
|
1309
|
+
} else if (framework === "nextjs") {
|
|
1310
|
+
if (filePath.endsWith("/page.tsx") || filePath.endsWith("/page.jsx")) {
|
|
1311
|
+
markers = NEXTJS_PAGE_MARKERS;
|
|
1312
|
+
}
|
|
1313
|
+
} else if (framework === "astro") {
|
|
1314
|
+
if (filePath.endsWith("/index.astro")) {
|
|
1315
|
+
markers = ASTRO_INDEX_MARKERS;
|
|
1316
|
+
}
|
|
1317
|
+
} else if (framework === "react-router") {
|
|
1318
|
+
if (filePath.endsWith("/home.tsx") || filePath.endsWith("/welcome.tsx")) {
|
|
1319
|
+
markers = REACT_ROUTER_HOME_MARKERS;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
if (markers === null) {
|
|
1323
|
+
if (size < 200) {
|
|
1324
|
+
return {
|
|
1325
|
+
path: filePath,
|
|
1326
|
+
exists: true,
|
|
1327
|
+
is_boilerplate: true,
|
|
1328
|
+
current_size: size,
|
|
1329
|
+
reason: "file is small (< 200 bytes), likely boilerplate or near-empty"
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
return {
|
|
1333
|
+
path: filePath,
|
|
1334
|
+
exists: true,
|
|
1335
|
+
is_boilerplate: false,
|
|
1336
|
+
current_size: size,
|
|
1337
|
+
reason: `file is ${size} bytes \u2014 no framework-default markers to verify, assuming user content`
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
const isDefault = looksLikeDefault(content, markers);
|
|
1341
|
+
return {
|
|
1342
|
+
path: filePath,
|
|
1343
|
+
exists: true,
|
|
1344
|
+
is_boilerplate: isDefault,
|
|
1345
|
+
current_size: size,
|
|
1346
|
+
reason: isDefault ? `matches ${framework} default scaffold markers` : `does not match ${framework} default scaffold \u2014 has user content`
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
function checkOverwriteSafetyBatch(root, framework, filePaths) {
|
|
1350
|
+
return filePaths.map((p2) => checkOverwriteSafety(root, framework, p2));
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// src/commands/apply.ts
|
|
1354
|
+
function isJsonMode4(args) {
|
|
1355
|
+
return args.flags.json === true || args.flags.json === "true";
|
|
1356
|
+
}
|
|
1357
|
+
function isForceMode(args) {
|
|
1358
|
+
return args.flags.force === true || args.flags.force === "true";
|
|
1359
|
+
}
|
|
1360
|
+
function isForceOverwriteMode(args) {
|
|
1361
|
+
return args.flags["force-overwrite"] === true || args.flags["force-overwrite"] === "true";
|
|
1362
|
+
}
|
|
1363
|
+
function isAutoInstallMode(args) {
|
|
1364
|
+
return args.flags["auto-install"] === true || args.flags["auto-install"] === "true";
|
|
1365
|
+
}
|
|
1366
|
+
function projectPath3(args) {
|
|
1367
|
+
const fromFlag = args.flags.path;
|
|
1368
|
+
if (typeof fromFlag === "string") return resolve4(fromFlag);
|
|
1369
|
+
return resolve4(process.cwd());
|
|
1370
|
+
}
|
|
1371
|
+
function getStringFlag2(args, name) {
|
|
1372
|
+
const v = args.flags[name];
|
|
1373
|
+
return typeof v === "string" ? v : null;
|
|
1374
|
+
}
|
|
1375
|
+
function resolveExperience2(experienceArg) {
|
|
1376
|
+
if (experienceArg === null) return null;
|
|
1377
|
+
if (experienceArg === "1" || experienceArg === "conversation-list") return 1;
|
|
1378
|
+
if (experienceArg === "2" || experienceArg === "one-to-one") return 2;
|
|
1379
|
+
if (experienceArg === "3" || experienceArg === "tab-based") return 3;
|
|
1380
|
+
const n = parseInt(experienceArg, 10);
|
|
1381
|
+
if (n >= 1 && n <= 3) return n;
|
|
1382
|
+
return null;
|
|
1383
|
+
}
|
|
1384
|
+
function experienceSlug(id) {
|
|
1385
|
+
switch (id) {
|
|
1386
|
+
case 1:
|
|
1387
|
+
return "conversation-list";
|
|
1388
|
+
case 2:
|
|
1389
|
+
return "one-to-one";
|
|
1390
|
+
case 3:
|
|
1391
|
+
return "tab-based";
|
|
1392
|
+
default:
|
|
1393
|
+
return "unknown";
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
function buildWriteOps(manifest, subs, sdkMajor, embedPath) {
|
|
1397
|
+
const ops = [];
|
|
1398
|
+
const ownedPaths = [];
|
|
1399
|
+
let entryPath = null;
|
|
1400
|
+
const moveMap = /* @__PURE__ */ new Map();
|
|
1401
|
+
if (embedPath !== null) {
|
|
1402
|
+
const embedDir = posixDirname(embedPath);
|
|
1403
|
+
for (const f of manifest.files) {
|
|
1404
|
+
if (f.action !== "create") continue;
|
|
1405
|
+
const defaultPath = substitute(f.path, subs);
|
|
1406
|
+
if (f.is_entry) {
|
|
1407
|
+
moveMap.set(defaultPath, embedPath);
|
|
1408
|
+
} else if (f.move_with_entry) {
|
|
1409
|
+
const basename = defaultPath.split("/").pop() ?? defaultPath;
|
|
1410
|
+
moveMap.set(defaultPath, posixJoin(embedDir, basename));
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
for (const f of manifest.files) {
|
|
1415
|
+
if (embedPath !== null && f.embed_skip) continue;
|
|
1416
|
+
if (f.action === "create") {
|
|
1417
|
+
const raw = readTemplateFile(sdkMajor, manifest.framework, manifest.experience_id, f.template_ref);
|
|
1418
|
+
let content = substitute(raw, subs);
|
|
1419
|
+
const defaultPath = substitute(f.path, subs);
|
|
1420
|
+
const path = moveMap.get(defaultPath) ?? defaultPath;
|
|
1421
|
+
if (embedPath !== null && moveMap.size > 0) {
|
|
1422
|
+
content = rewriteImportsThroughMoveMap(content, defaultPath, path, moveMap);
|
|
1423
|
+
}
|
|
1424
|
+
if (f.is_entry) entryPath = path;
|
|
1425
|
+
const owned = f.owned ?? true;
|
|
1426
|
+
ops.push({ type: "create", path, content, owned });
|
|
1427
|
+
if (owned) ownedPaths.push(path);
|
|
1428
|
+
} else if (f.action === "patch") {
|
|
1429
|
+
const path = substitute(f.path, subs);
|
|
1430
|
+
const patchContent = f.patch_content ? substitute(f.patch_content, subs) : "";
|
|
1431
|
+
ops.push({
|
|
1432
|
+
type: "patch",
|
|
1433
|
+
path,
|
|
1434
|
+
patch_mode: f.patch_mode ?? "prepend",
|
|
1435
|
+
patch_content: patchContent,
|
|
1436
|
+
anchor: f.anchor,
|
|
1437
|
+
skip_if_exists: f.skip_if_exists,
|
|
1438
|
+
patch_id: `${manifest.experience_id}/${f.path}`
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
return { ops, ownedPaths, entryPath };
|
|
1443
|
+
}
|
|
1444
|
+
function parsePlacement(raw) {
|
|
1445
|
+
if (raw.startsWith("embed:")) {
|
|
1446
|
+
const path = raw.slice("embed:".length).trim();
|
|
1447
|
+
return { placement: "embed", embedPath: path.length > 0 ? path : null };
|
|
1448
|
+
}
|
|
1449
|
+
return { placement: raw, embedPath: null };
|
|
1450
|
+
}
|
|
1451
|
+
function rewriteImportsThroughMoveMap(content, oldFilePath, newFilePath, moveMap) {
|
|
1452
|
+
const oldDir = posixDirname(oldFilePath);
|
|
1453
|
+
const newDir = posixDirname(newFilePath);
|
|
1454
|
+
const importRe = /(\bfrom\s+["']|\bimport\s+["']|\bimport\(\s*["'])(\.{1,2}\/[^"']+)(["'])/g;
|
|
1455
|
+
const tryExts = ["", ".tsx", ".ts", ".jsx", ".js"];
|
|
1456
|
+
return content.replace(importRe, (_match, prefix, importPath, suffix) => {
|
|
1457
|
+
const resolvedFromOld = posixJoin(oldDir, importPath);
|
|
1458
|
+
let movedTarget = null;
|
|
1459
|
+
let usedExt = "";
|
|
1460
|
+
for (const ext of tryExts) {
|
|
1461
|
+
const moved = moveMap.get(resolvedFromOld + ext);
|
|
1462
|
+
if (moved !== void 0) {
|
|
1463
|
+
movedTarget = moved;
|
|
1464
|
+
usedExt = ext;
|
|
1465
|
+
break;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
const targetAbs = movedTarget !== null ? usedExt ? movedTarget.slice(0, movedTarget.length - usedExt.length) : movedTarget : resolvedFromOld;
|
|
1469
|
+
let newRel = posixRelative(newDir, targetAbs);
|
|
1470
|
+
if (newRel.length === 0) newRel = ".";
|
|
1471
|
+
if (!newRel.startsWith(".")) newRel = "./" + newRel;
|
|
1472
|
+
return prefix + newRel + suffix;
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
function embedNextSteps(framework, embedPath) {
|
|
1476
|
+
switch (framework) {
|
|
1477
|
+
case "reactjs":
|
|
1478
|
+
return [
|
|
1479
|
+
`Embed mode: the chat component was written to ${embedPath} (your existing main.tsx was NOT touched).`,
|
|
1480
|
+
`Wire it up by importing it from wherever you mount your app:`,
|
|
1481
|
+
` import App from "./${embedPath.replace(/^src\//, "").replace(/\.tsx?$/, "")}";`,
|
|
1482
|
+
` // Then render <App /> from your existing root.`
|
|
1483
|
+
];
|
|
1484
|
+
case "nextjs":
|
|
1485
|
+
return [
|
|
1486
|
+
`Embed mode: the chat page was written to ${embedPath}.`,
|
|
1487
|
+
`Next.js auto-routes by file location \u2014 visit the URL that matches the path under src/app/.`,
|
|
1488
|
+
`Example: ${embedPath} \u2192 ${embedPath.replace(/^src\/app/, "").replace(/\/page\.tsx?$/, "") || "/"}`
|
|
1489
|
+
];
|
|
1490
|
+
case "react-router":
|
|
1491
|
+
return [
|
|
1492
|
+
`Embed mode: the route file was written to ${embedPath} (app/routes.ts was NOT patched).`,
|
|
1493
|
+
`Add the route manually to app/routes.ts:`,
|
|
1494
|
+
` import { type RouteConfig, index, route } from "@react-router/dev/routes";`,
|
|
1495
|
+
` export default [`,
|
|
1496
|
+
` index("routes/home.tsx"),`,
|
|
1497
|
+
` route("chat", "${embedPath.replace(/^app\//, "")}"),`,
|
|
1498
|
+
` ] satisfies RouteConfig;`
|
|
1499
|
+
];
|
|
1500
|
+
case "astro":
|
|
1501
|
+
return [
|
|
1502
|
+
`Embed mode: the page file was written to ${embedPath}.`,
|
|
1503
|
+
`Astro auto-routes pages under src/pages/ \u2014 visit the URL that matches the path.`,
|
|
1504
|
+
`Example: ${embedPath} \u2192 ${embedPath.replace(/^src\/pages/, "").replace(/\.astro$/, "") || "/"}`
|
|
1505
|
+
];
|
|
1506
|
+
default:
|
|
1507
|
+
return [`Embed mode: chat entry was written to ${embedPath}.`];
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
async function apply(args) {
|
|
1511
|
+
const root = projectPath3(args);
|
|
1512
|
+
const experienceArg = getStringFlag2(args, "experience");
|
|
1513
|
+
const frameworkArg = getStringFlag2(args, "framework");
|
|
1514
|
+
const placementRaw = getStringFlag2(args, "placement") ?? "spa-root";
|
|
1515
|
+
const { placement, embedPath } = parsePlacement(placementRaw);
|
|
1516
|
+
if (placementRaw.startsWith("embed:") && embedPath === null) {
|
|
1517
|
+
return errorOut(args, "Error: --placement embed: requires a path (e.g. --placement embed:src/pages/chat.tsx)", 1);
|
|
1518
|
+
}
|
|
1519
|
+
const sdkMajor = 6;
|
|
1520
|
+
const experienceId = resolveExperience2(experienceArg);
|
|
1521
|
+
if (experienceId === null) {
|
|
1522
|
+
return errorOut(args, "Error: --experience is required (1, 2, 3, or slug)", 1);
|
|
1523
|
+
}
|
|
1524
|
+
let framework = frameworkArg;
|
|
1525
|
+
let envPrefix = null;
|
|
1526
|
+
let frameworkVersion = null;
|
|
1527
|
+
let credentialsSource = "none";
|
|
1528
|
+
let detectedRouter = null;
|
|
1529
|
+
if (!framework) {
|
|
1530
|
+
const detected = await runDetectors(root);
|
|
1531
|
+
if (detected.framework === null) {
|
|
1532
|
+
return errorOut(
|
|
1533
|
+
args,
|
|
1534
|
+
"Error: --framework not specified and no React framework detected in " + root,
|
|
1535
|
+
1
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
framework = detected.framework;
|
|
1539
|
+
envPrefix = detected.env_prefix;
|
|
1540
|
+
frameworkVersion = detected.framework_version;
|
|
1541
|
+
credentialsSource = detected.credentials.source;
|
|
1542
|
+
detectedRouter = detected.router ?? null;
|
|
1543
|
+
} else if (framework === "nextjs") {
|
|
1544
|
+
const detected = await runDetectors(root);
|
|
1545
|
+
detectedRouter = detected.router ?? null;
|
|
1546
|
+
if (!frameworkVersion) frameworkVersion = detected.framework_version;
|
|
1547
|
+
if (credentialsSource === "none") credentialsSource = detected.credentials.source;
|
|
1548
|
+
}
|
|
1549
|
+
if (!envPrefix) {
|
|
1550
|
+
envPrefix = framework === "nextjs" ? "NEXT_PUBLIC_" : framework === "astro" ? "PUBLIC_" : "VITE_";
|
|
1551
|
+
}
|
|
1552
|
+
let manifestKey = framework;
|
|
1553
|
+
if (framework === "nextjs" && detectedRouter === "pages") {
|
|
1554
|
+
manifestKey = "nextjs-pages";
|
|
1555
|
+
if (experienceId !== 1) {
|
|
1556
|
+
return errorOut(
|
|
1557
|
+
args,
|
|
1558
|
+
`Error: Next.js Pages Router currently supports experience 1 (Conversation List) only. Experience ${experienceId} for Pages Router lands in v2.0.1. Options: (1) use experience 1, (2) migrate your project to App Router (recommended for new Next.js projects), or (3) wait for v2.0.1.`,
|
|
1559
|
+
1
|
|
1560
|
+
);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
if (hasState(root)) {
|
|
1564
|
+
const existing = readState(root);
|
|
1565
|
+
if (existing) {
|
|
1566
|
+
if (isSameIntegration(existing, {
|
|
1567
|
+
experience: experienceId,
|
|
1568
|
+
framework,
|
|
1569
|
+
placement
|
|
1570
|
+
})) {
|
|
1571
|
+
const result2 = {
|
|
1572
|
+
status: "already-applied",
|
|
1573
|
+
framework,
|
|
1574
|
+
experience_id: experienceId,
|
|
1575
|
+
experience_slug: experienceSlug(experienceId),
|
|
1576
|
+
files_created: [],
|
|
1577
|
+
files_modified: [],
|
|
1578
|
+
files_skipped: [],
|
|
1579
|
+
deps_to_install: [],
|
|
1580
|
+
env_vars_to_add: [],
|
|
1581
|
+
state_file: ".cometchat/state.json",
|
|
1582
|
+
next_steps: [
|
|
1583
|
+
"Integration is already in place \u2014 no changes made.",
|
|
1584
|
+
"Run `cometchat info` to see current integration state."
|
|
1585
|
+
]
|
|
1586
|
+
};
|
|
1587
|
+
return outputResult(args, result2, 0);
|
|
1588
|
+
}
|
|
1589
|
+
if (!isForceMode(args)) {
|
|
1590
|
+
const result2 = {
|
|
1591
|
+
status: "conflict",
|
|
1592
|
+
framework,
|
|
1593
|
+
experience_id: experienceId,
|
|
1594
|
+
experience_slug: experienceSlug(experienceId),
|
|
1595
|
+
files_created: [],
|
|
1596
|
+
files_modified: [],
|
|
1597
|
+
files_skipped: [],
|
|
1598
|
+
deps_to_install: [],
|
|
1599
|
+
env_vars_to_add: [],
|
|
1600
|
+
state_file: ".cometchat/state.json",
|
|
1601
|
+
next_steps: [
|
|
1602
|
+
`An existing integration is in place: experience ${existing.experience} (${existing.experience_slug}) on ${existing.framework}.`,
|
|
1603
|
+
`You requested experience ${experienceId} (${experienceSlug(experienceId)}) \u2014 these conflict.`,
|
|
1604
|
+
"Run with --force to replace, or run `cometchat uninstall` first."
|
|
1605
|
+
]
|
|
1606
|
+
};
|
|
1607
|
+
return outputResult(args, result2, 2);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
let manifest;
|
|
1612
|
+
try {
|
|
1613
|
+
manifest = loadManifest(sdkMajor, manifestKey, experienceId);
|
|
1614
|
+
} catch (err) {
|
|
1615
|
+
return errorOut(
|
|
1616
|
+
args,
|
|
1617
|
+
"Error: " + (err instanceof Error ? err.message : String(err)),
|
|
1618
|
+
1
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
const subs = defaultSubs(envPrefix);
|
|
1622
|
+
const { ops, ownedPaths, entryPath } = buildWriteOps(manifest, subs, sdkMajor, embedPath);
|
|
1623
|
+
if (embedPath !== null && entryPath === null) {
|
|
1624
|
+
return errorOut(
|
|
1625
|
+
args,
|
|
1626
|
+
`Error: --placement embed:<path> is not supported for ${framework} experience ${experienceId} (no entry file declared in the manifest).`,
|
|
1627
|
+
1
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
if (!isForceOverwriteMode(args)) {
|
|
1631
|
+
const createPaths = ops.filter((o) => o.type === "create").map((o) => o.path);
|
|
1632
|
+
const safetyReports = checkOverwriteSafetyBatch(root, framework, createPaths);
|
|
1633
|
+
const unsafe = safetyReports.filter((r) => r.exists && !r.is_boilerplate);
|
|
1634
|
+
if (unsafe.length > 0) {
|
|
1635
|
+
const result2 = {
|
|
1636
|
+
status: "error",
|
|
1637
|
+
framework,
|
|
1638
|
+
experience_id: experienceId,
|
|
1639
|
+
experience_slug: experienceSlug(experienceId),
|
|
1640
|
+
files_created: [],
|
|
1641
|
+
files_modified: [],
|
|
1642
|
+
files_skipped: unsafe.map((u) => ({
|
|
1643
|
+
path: u.path,
|
|
1644
|
+
reason: `would overwrite user content (${u.current_size} bytes) \u2014 ${u.reason}`
|
|
1645
|
+
})),
|
|
1646
|
+
deps_to_install: [],
|
|
1647
|
+
env_vars_to_add: [],
|
|
1648
|
+
state_file: ".cometchat/state.json",
|
|
1649
|
+
next_steps: [
|
|
1650
|
+
`Refusing to overwrite ${unsafe.length} file(s) that contain user code.`,
|
|
1651
|
+
"Options:",
|
|
1652
|
+
" 1. Run with --force-overwrite to replace these files (DESTRUCTIVE).",
|
|
1653
|
+
" 2. Edit the files manually to remove the existing content first.",
|
|
1654
|
+
" 3. Use --placement embed:<custom-path> to write the entry file to a different location (e.g. --placement embed:src/pages/chat.tsx)."
|
|
1655
|
+
],
|
|
1656
|
+
error: `Refusing to overwrite ${unsafe.length} file(s) with user content. Use --force-overwrite to override.`
|
|
1657
|
+
};
|
|
1658
|
+
return outputResult(args, result2, 2);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
let writeResult;
|
|
1662
|
+
try {
|
|
1663
|
+
writeResult = applyWrites(root, ops);
|
|
1664
|
+
} catch (err) {
|
|
1665
|
+
return errorOut(
|
|
1666
|
+
args,
|
|
1667
|
+
"Apply failed (rolled back): " + (err instanceof Error ? err.message : String(err)),
|
|
1668
|
+
1
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
1671
|
+
const filesPatched = manifest.files.filter((f) => f.action === "patch").map((f) => ({
|
|
1672
|
+
path: substitute(f.path, subs),
|
|
1673
|
+
patch_id: `${manifest.experience_id}/${f.path}`
|
|
1674
|
+
}));
|
|
1675
|
+
const state = buildState({
|
|
1676
|
+
framework,
|
|
1677
|
+
framework_version: frameworkVersion,
|
|
1678
|
+
experience: experienceId,
|
|
1679
|
+
experience_slug: experienceSlug(experienceId),
|
|
1680
|
+
placement,
|
|
1681
|
+
placement_path: embedPath ?? void 0,
|
|
1682
|
+
template_version: manifest.version,
|
|
1683
|
+
sdk_version: null,
|
|
1684
|
+
// detected lazily — not yet integrated
|
|
1685
|
+
credentials_source: credentialsSource,
|
|
1686
|
+
env_var_prefix: envPrefix,
|
|
1687
|
+
files_owned: ownedPaths,
|
|
1688
|
+
files_patched: filesPatched,
|
|
1689
|
+
checksums: writeResult.checksums,
|
|
1690
|
+
// For nextjs: capture App Router vs Pages Router so consumers
|
|
1691
|
+
// (production-auth, add-user-mgmt, add-widget) can pick the right
|
|
1692
|
+
// file paths. Other frameworks omit this.
|
|
1693
|
+
router: framework === "nextjs" ? detectedRouter === "pages" ? "pages" : "app" : void 0
|
|
1694
|
+
});
|
|
1695
|
+
writeState(root, state);
|
|
1696
|
+
const result = {
|
|
1697
|
+
status: "applied",
|
|
1698
|
+
framework,
|
|
1699
|
+
experience_id: experienceId,
|
|
1700
|
+
experience_slug: experienceSlug(experienceId),
|
|
1701
|
+
files_created: writeResult.files_created,
|
|
1702
|
+
files_modified: writeResult.files_modified,
|
|
1703
|
+
files_skipped: writeResult.files_skipped,
|
|
1704
|
+
deps_to_install: manifest.deps.map((d) => ({
|
|
1705
|
+
name: d.name,
|
|
1706
|
+
version: d.version,
|
|
1707
|
+
dev: d.dev ?? false
|
|
1708
|
+
})),
|
|
1709
|
+
env_vars_to_add: manifest.env_vars.map((v) => ({
|
|
1710
|
+
name: substitute(v.name, subs),
|
|
1711
|
+
required: v.required,
|
|
1712
|
+
secret: v.secret ?? false
|
|
1713
|
+
})),
|
|
1714
|
+
state_file: ".cometchat/state.json",
|
|
1715
|
+
next_steps: (manifest.next_steps ?? []).map((s) => substitute(s, subs))
|
|
1716
|
+
};
|
|
1717
|
+
if (embedPath !== null && entryPath !== null) {
|
|
1718
|
+
const embedHints = embedNextSteps(framework, embedPath);
|
|
1719
|
+
result.next_steps = [...embedHints, ...result.next_steps];
|
|
1720
|
+
}
|
|
1721
|
+
if (isAutoInstallMode(args) && manifest.deps.length > 0) {
|
|
1722
|
+
result.install = runAutoInstall(root, manifest.deps);
|
|
1723
|
+
}
|
|
1724
|
+
appendAuditEntry(root, {
|
|
1725
|
+
command: "apply",
|
|
1726
|
+
summary: `Applied experience ${experienceId} (${experienceSlug(experienceId)}) on ${framework}${detectedRouter ? ` (${detectedRouter} router)` : ""}${embedPath ? ` with --placement embed:${embedPath}` : ""}`,
|
|
1727
|
+
inputs: {
|
|
1728
|
+
framework,
|
|
1729
|
+
...detectedRouter ? { router: detectedRouter } : {},
|
|
1730
|
+
experience: experienceId,
|
|
1731
|
+
placement: embedPath ? `embed:${embedPath}` : placement,
|
|
1732
|
+
env_var_prefix: envPrefix
|
|
1733
|
+
},
|
|
1734
|
+
decisions: {
|
|
1735
|
+
framework: frameworkArg ? `passed explicitly via --framework ${frameworkArg}` : `auto-detected from package.json + project structure`,
|
|
1736
|
+
...framework === "nextjs" && detectedRouter ? {
|
|
1737
|
+
router: detectedRouter === "pages" ? `Pages Router detected (src/pages/ exists, src/app/ does not) \u2014 using nextjs-pages template variant` : `App Router detected (src/app/ exists) \u2014 using nextjs template variant`
|
|
1738
|
+
} : {},
|
|
1739
|
+
...embedPath ? {
|
|
1740
|
+
placement: `--placement embed:${embedPath} \u2014 entry file relocated; default wire-up files (e.g. main.tsx, routes.ts patches) skipped via embed_skip; companion files (App.css, etc.) moved alongside; relative imports rewritten via the moveMap so each one resolves to the same on-disk target`
|
|
1741
|
+
} : {},
|
|
1742
|
+
template_version: `loaded from @cometchat/skills-registry v${sdkMajor} \u2014 manifest version ${manifest.version}, sdk_compatibility ${manifest.sdk_compatibility}`
|
|
1743
|
+
},
|
|
1744
|
+
files_created: writeResult.files_created,
|
|
1745
|
+
files_patched: filesPatched.map((f) => f.path),
|
|
1746
|
+
files_skipped: writeResult.files_skipped.map((s) => s.path),
|
|
1747
|
+
next_actions: result.next_steps
|
|
1748
|
+
});
|
|
1749
|
+
return outputResult(args, result, 0);
|
|
1750
|
+
}
|
|
1751
|
+
function runAutoInstall(root, deps) {
|
|
1752
|
+
const pm = detectPackageManager(root) ?? "npm";
|
|
1753
|
+
const args = pm === "npm" ? ["install"] : ["add"];
|
|
1754
|
+
const depArgs = deps.map((d) => `${d.name}@${d.version}`);
|
|
1755
|
+
const start = Date.now();
|
|
1756
|
+
const proc = spawnSync(pm, [...args, ...depArgs], {
|
|
1757
|
+
cwd: root,
|
|
1758
|
+
stdio: "ignore",
|
|
1759
|
+
encoding: "utf8"
|
|
1760
|
+
});
|
|
1761
|
+
const duration = Date.now() - start;
|
|
1762
|
+
if (proc.error) {
|
|
1763
|
+
return {
|
|
1764
|
+
status: "error",
|
|
1765
|
+
package_manager: pm,
|
|
1766
|
+
duration_ms: duration,
|
|
1767
|
+
error: proc.error.message
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
if (proc.status !== 0) {
|
|
1771
|
+
return {
|
|
1772
|
+
status: "error",
|
|
1773
|
+
package_manager: pm,
|
|
1774
|
+
duration_ms: duration,
|
|
1775
|
+
error: `${pm} exited with code ${proc.status}`
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
return {
|
|
1779
|
+
status: "installed",
|
|
1780
|
+
package_manager: pm,
|
|
1781
|
+
duration_ms: duration
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
function outputResult(args, result, exitCode) {
|
|
1785
|
+
if (isJsonMode4(args)) {
|
|
1786
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1787
|
+
return exitCode;
|
|
1788
|
+
}
|
|
1789
|
+
printHumanReadable3(result);
|
|
1790
|
+
return exitCode;
|
|
1791
|
+
}
|
|
1792
|
+
function errorOut(args, message, exitCode) {
|
|
1793
|
+
if (isJsonMode4(args)) {
|
|
1794
|
+
console.log(JSON.stringify({ status: "error", error: message }, null, 2));
|
|
1795
|
+
} else {
|
|
1796
|
+
console.error(message);
|
|
1797
|
+
}
|
|
1798
|
+
return exitCode;
|
|
1799
|
+
}
|
|
1800
|
+
function printHumanReadable3(r) {
|
|
1801
|
+
const lines = [];
|
|
1802
|
+
lines.push("");
|
|
1803
|
+
if (r.status === "applied") {
|
|
1804
|
+
lines.push(` \u2713 Applied: experience ${r.experience_id} (${r.experience_slug}) on ${r.framework}`);
|
|
1805
|
+
} else if (r.status === "already-applied") {
|
|
1806
|
+
lines.push(` = Already applied: experience ${r.experience_id} (${r.experience_slug}) on ${r.framework}`);
|
|
1807
|
+
} else if (r.status === "conflict") {
|
|
1808
|
+
lines.push(` \u2717 Conflict: a different integration is already in place`);
|
|
1809
|
+
}
|
|
1810
|
+
lines.push("");
|
|
1811
|
+
if (r.files_created.length > 0) {
|
|
1812
|
+
lines.push(" Files created:");
|
|
1813
|
+
for (const f of r.files_created) lines.push(` + ${f}`);
|
|
1814
|
+
}
|
|
1815
|
+
if (r.files_modified.length > 0) {
|
|
1816
|
+
lines.push(" Files modified:");
|
|
1817
|
+
for (const f of r.files_modified) lines.push(` ~ ${f}`);
|
|
1818
|
+
}
|
|
1819
|
+
if (r.files_skipped.length > 0) {
|
|
1820
|
+
lines.push(" Files skipped:");
|
|
1821
|
+
for (const f of r.files_skipped) lines.push(` - ${f.path} (${f.reason})`);
|
|
1822
|
+
}
|
|
1823
|
+
if (r.deps_to_install.length > 0) {
|
|
1824
|
+
if (r.install?.status === "installed") {
|
|
1825
|
+
lines.push("");
|
|
1826
|
+
lines.push(
|
|
1827
|
+
` \u2713 Auto-installed ${r.deps_to_install.length} package(s) via ${r.install.package_manager} (${r.install.duration_ms}ms)`
|
|
1828
|
+
);
|
|
1829
|
+
} else if (r.install?.status === "error") {
|
|
1830
|
+
lines.push("");
|
|
1831
|
+
lines.push(
|
|
1832
|
+
` \u26A0 Auto-install FAILED via ${r.install.package_manager}: ${r.install.error ?? "unknown error"}`
|
|
1833
|
+
);
|
|
1834
|
+
lines.push(" You can retry with: cometchat install");
|
|
1835
|
+
} else {
|
|
1836
|
+
lines.push("");
|
|
1837
|
+
lines.push(" Run this to install dependencies:");
|
|
1838
|
+
const depList = r.deps_to_install.map((d) => `${d.name}@${d.version}`).join(" ");
|
|
1839
|
+
lines.push(` npm install ${depList}`);
|
|
1840
|
+
lines.push(" (or pass --auto-install next time to do this automatically)");
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
if (r.env_vars_to_add.length > 0) {
|
|
1844
|
+
lines.push("");
|
|
1845
|
+
lines.push(" Add these to your .env file:");
|
|
1846
|
+
for (const e of r.env_vars_to_add) {
|
|
1847
|
+
const tag = e.secret ? " # secret \u2014 do not commit" : "";
|
|
1848
|
+
lines.push(` ${e.name}=YOUR_VALUE_HERE${tag}`);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
lines.push("");
|
|
1852
|
+
lines.push(` State file: ${r.state_file}`);
|
|
1853
|
+
if (r.next_steps.length > 0) {
|
|
1854
|
+
lines.push("");
|
|
1855
|
+
lines.push(" Next steps:");
|
|
1856
|
+
for (const s of r.next_steps) lines.push(` - ${s}`);
|
|
1857
|
+
}
|
|
1858
|
+
lines.push("");
|
|
1859
|
+
console.log(lines.join("\n"));
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
// src/commands/info.ts
|
|
1863
|
+
import { resolve as resolve5 } from "node:path";
|
|
1864
|
+
function isJsonMode5(args) {
|
|
1865
|
+
return args.flags.json === true || args.flags.json === "true";
|
|
1866
|
+
}
|
|
1867
|
+
function projectPath4(args) {
|
|
1868
|
+
const fromFlag = args.flags.path;
|
|
1869
|
+
if (typeof fromFlag === "string") return resolve5(fromFlag);
|
|
1870
|
+
return resolve5(process.cwd());
|
|
1871
|
+
}
|
|
1872
|
+
async function info(args) {
|
|
1873
|
+
const root = projectPath4(args);
|
|
1874
|
+
if (!hasState(root)) {
|
|
1875
|
+
const result2 = { integrated: false };
|
|
1876
|
+
return outputResult2(args, result2, 0);
|
|
1877
|
+
}
|
|
1878
|
+
const state = readState(root);
|
|
1879
|
+
if (!state) {
|
|
1880
|
+
if (isJsonMode5(args)) {
|
|
1881
|
+
console.log(
|
|
1882
|
+
JSON.stringify(
|
|
1883
|
+
{
|
|
1884
|
+
integrated: false,
|
|
1885
|
+
error: "state file exists but could not be parsed (schema mismatch?)"
|
|
1886
|
+
},
|
|
1887
|
+
null,
|
|
1888
|
+
2
|
|
1889
|
+
)
|
|
1890
|
+
);
|
|
1891
|
+
} else {
|
|
1892
|
+
console.error("Error: .cometchat/state.json exists but could not be parsed.");
|
|
1893
|
+
}
|
|
1894
|
+
return 1;
|
|
1895
|
+
}
|
|
1896
|
+
const drift = detectDrift(root, state);
|
|
1897
|
+
const auditLogExists = pathExists(p(root, AUDIT_LOG_PATH));
|
|
1898
|
+
const result = {
|
|
1899
|
+
integrated: true,
|
|
1900
|
+
experience: state.experience,
|
|
1901
|
+
experience_slug: state.experience_slug,
|
|
1902
|
+
framework: state.framework,
|
|
1903
|
+
framework_version: state.framework_version,
|
|
1904
|
+
applied_at: state.applied_at,
|
|
1905
|
+
template_version: state.template_version,
|
|
1906
|
+
placement: state.placement,
|
|
1907
|
+
...state.router ? { router: state.router } : {},
|
|
1908
|
+
...state.applied_features && state.applied_features.length > 0 ? { applied_features: state.applied_features } : {},
|
|
1909
|
+
drift: {
|
|
1910
|
+
modified_files: drift.modified_files,
|
|
1911
|
+
missing_files: drift.missing_files,
|
|
1912
|
+
unchanged_files: drift.unchanged_files,
|
|
1913
|
+
has_drift: drift.modified_files.length > 0 || drift.missing_files.length > 0
|
|
1914
|
+
},
|
|
1915
|
+
files_owned: state.files_owned,
|
|
1916
|
+
files_patched: state.files_patched,
|
|
1917
|
+
audit_log: auditLogExists ? AUDIT_LOG_PATH : null
|
|
1918
|
+
};
|
|
1919
|
+
return outputResult2(args, result, 0);
|
|
1920
|
+
}
|
|
1921
|
+
function outputResult2(args, result, exitCode) {
|
|
1922
|
+
if (isJsonMode5(args)) {
|
|
1923
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1924
|
+
return exitCode;
|
|
1925
|
+
}
|
|
1926
|
+
printHumanReadable4(result);
|
|
1927
|
+
return exitCode;
|
|
1928
|
+
}
|
|
1929
|
+
function printHumanReadable4(r) {
|
|
1930
|
+
const lines = [];
|
|
1931
|
+
lines.push("");
|
|
1932
|
+
if (!r.integrated) {
|
|
1933
|
+
lines.push(" CometChat: not integrated in this project.");
|
|
1934
|
+
lines.push(" Run `cometchat apply --experience N` to integrate.");
|
|
1935
|
+
lines.push("");
|
|
1936
|
+
console.log(lines.join("\n"));
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
lines.push(` \u2713 Integrated: experience ${r.experience} (${r.experience_slug}) on ${r.framework}${r.router ? ` (${r.router} router)` : ""}`);
|
|
1940
|
+
if (r.framework_version) lines.push(` Framework version: ${r.framework_version}`);
|
|
1941
|
+
if (r.template_version) lines.push(` Template version: ${r.template_version}`);
|
|
1942
|
+
if (r.placement) lines.push(` Placement: ${r.placement}`);
|
|
1943
|
+
if (r.applied_at) lines.push(` Applied at: ${r.applied_at}`);
|
|
1944
|
+
if (r.applied_features && r.applied_features.length > 0) {
|
|
1945
|
+
lines.push(` Applied features: ${r.applied_features.join(", ")}`);
|
|
1946
|
+
}
|
|
1947
|
+
lines.push("");
|
|
1948
|
+
if (r.drift) {
|
|
1949
|
+
if (r.drift.has_drift) {
|
|
1950
|
+
lines.push(` \u26A0 Drift detected:`);
|
|
1951
|
+
if (r.drift.modified_files.length > 0) {
|
|
1952
|
+
lines.push(` Modified by user (${r.drift.modified_files.length}):`);
|
|
1953
|
+
for (const f of r.drift.modified_files) lines.push(` ~ ${f}`);
|
|
1954
|
+
}
|
|
1955
|
+
if (r.drift.missing_files.length > 0) {
|
|
1956
|
+
lines.push(` Missing (${r.drift.missing_files.length}):`);
|
|
1957
|
+
for (const f of r.drift.missing_files) lines.push(` - ${f}`);
|
|
1958
|
+
}
|
|
1959
|
+
lines.push("");
|
|
1960
|
+
lines.push(" Run `cometchat apply --force` to restore the original templates,");
|
|
1961
|
+
lines.push(" or `cometchat uninstall --force` to remove the integration.");
|
|
1962
|
+
} else {
|
|
1963
|
+
lines.push(` \u2713 No drift \u2014 all ${r.drift.unchanged_files.length} owned files unchanged since apply.`);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
if (r.audit_log) {
|
|
1967
|
+
lines.push("");
|
|
1968
|
+
lines.push(` Audit log: ${r.audit_log} (human-readable history of every CometChat command run)`);
|
|
1969
|
+
}
|
|
1970
|
+
lines.push("");
|
|
1971
|
+
console.log(lines.join("\n"));
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
// src/commands/verify.ts
|
|
1975
|
+
import { resolve as resolve6 } from "node:path";
|
|
1976
|
+
function isJsonMode6(args) {
|
|
1977
|
+
return args.flags.json === true || args.flags.json === "true";
|
|
1978
|
+
}
|
|
1979
|
+
function projectPath5(args) {
|
|
1980
|
+
const fromFlag = args.flags.path;
|
|
1981
|
+
if (typeof fromFlag === "string") return resolve6(fromFlag);
|
|
1982
|
+
return resolve6(process.cwd());
|
|
1983
|
+
}
|
|
1984
|
+
function checkCssVariablesImport(root) {
|
|
1985
|
+
const candidates = ["src/index.css", "src/main.css", "src/styles.css", "src/app/globals.css"];
|
|
1986
|
+
let count = 0;
|
|
1987
|
+
for (const c of candidates) {
|
|
1988
|
+
const content = readFileOrNull(p(root, c));
|
|
1989
|
+
if (content && content.includes("@cometchat/chat-uikit-react/css-variables.css")) {
|
|
1990
|
+
count++;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
if (count === 0) {
|
|
1994
|
+
return {
|
|
1995
|
+
status: "fail",
|
|
1996
|
+
reason: "css-variables.css is not imported in any known stylesheet"
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
if (count > 1) {
|
|
2000
|
+
return {
|
|
2001
|
+
status: "fail",
|
|
2002
|
+
reason: `css-variables.css is imported ${count} times \u2014 should be exactly once`
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
return { status: "pass" };
|
|
2006
|
+
}
|
|
2007
|
+
function checkNoAuthKeyInSource(root, ownedFiles) {
|
|
2008
|
+
const HEX_KEY = /[a-f0-9]{32,}/;
|
|
2009
|
+
for (const file of ownedFiles) {
|
|
2010
|
+
const content = readFileOrNull(p(root, file));
|
|
2011
|
+
if (!content) continue;
|
|
2012
|
+
if (HEX_KEY.test(content)) {
|
|
2013
|
+
const matches = content.match(HEX_KEY);
|
|
2014
|
+
if (matches && matches[0].length >= 40) {
|
|
2015
|
+
return {
|
|
2016
|
+
status: "fail",
|
|
2017
|
+
reason: `Possible hardcoded Auth Key in ${file}`,
|
|
2018
|
+
details: `Found a long hex string (${matches[0].length} chars) \u2014 verify it's not a credential`
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
return { status: "pass" };
|
|
2024
|
+
}
|
|
2025
|
+
function checkRenderGatedOnLogin(root) {
|
|
2026
|
+
const candidates = ["src/main.tsx", "src/main.jsx", "src/index.tsx", "src/index.jsx"];
|
|
2027
|
+
for (const c of candidates) {
|
|
2028
|
+
const content = readFileOrNull(p(root, c));
|
|
2029
|
+
if (!content) continue;
|
|
2030
|
+
const hasMount = /function\s+mount\s*\(\s*\)/.test(content);
|
|
2031
|
+
const hasInit = /CometChatUIKit\.init/.test(content);
|
|
2032
|
+
if (!hasInit) {
|
|
2033
|
+
return { status: "skip", reason: `${c} does not contain CometChatUIKit.init` };
|
|
2034
|
+
}
|
|
2035
|
+
if (!hasMount) {
|
|
2036
|
+
return {
|
|
2037
|
+
status: "fail",
|
|
2038
|
+
reason: `${c} should wrap createRoot(...).render in a mount() function called only after login resolves`
|
|
2039
|
+
};
|
|
2040
|
+
}
|
|
2041
|
+
return { status: "pass" };
|
|
2042
|
+
}
|
|
2043
|
+
return { status: "skip", reason: "no entry file found" };
|
|
2044
|
+
}
|
|
2045
|
+
function checkInitBeforeLogin(root) {
|
|
2046
|
+
const candidates = ["src/main.tsx", "src/main.jsx", "src/index.tsx", "src/index.jsx", "src/cometchat/CometChatNoSSR.tsx"];
|
|
2047
|
+
for (const c of candidates) {
|
|
2048
|
+
const content = readFileOrNull(p(root, c));
|
|
2049
|
+
if (!content) continue;
|
|
2050
|
+
if (!content.includes("CometChatUIKit.init")) continue;
|
|
2051
|
+
const initIdx = content.indexOf("CometChatUIKit.init");
|
|
2052
|
+
const loginIdx = content.indexOf("CometChatUIKit.login");
|
|
2053
|
+
if (loginIdx === -1) {
|
|
2054
|
+
return { status: "skip", reason: `no login call found in ${c}` };
|
|
2055
|
+
}
|
|
2056
|
+
if (initIdx > loginIdx) {
|
|
2057
|
+
return {
|
|
2058
|
+
status: "fail",
|
|
2059
|
+
reason: `${c}: CometChatUIKit.login appears before CometChatUIKit.init`
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
return { status: "pass" };
|
|
2063
|
+
}
|
|
2064
|
+
return { status: "skip", reason: "no entry file found" };
|
|
2065
|
+
}
|
|
2066
|
+
function checkErrorUiVisible(root, ownedFiles) {
|
|
2067
|
+
for (const file of ownedFiles) {
|
|
2068
|
+
const content = readFileOrNull(p(root, file));
|
|
2069
|
+
if (!content) continue;
|
|
2070
|
+
if (/color:\s*['"]?red/.test(content)) {
|
|
2071
|
+
return { status: "pass" };
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
return {
|
|
2075
|
+
status: "fail",
|
|
2076
|
+
reason: "No error UI (color: red) found in any owned file"
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
async function verify(args) {
|
|
2080
|
+
const root = projectPath5(args);
|
|
2081
|
+
if (!hasState(root)) {
|
|
2082
|
+
if (isJsonMode6(args)) {
|
|
2083
|
+
console.log(
|
|
2084
|
+
JSON.stringify({ status: "fail", error: "no integration found" }, null, 2)
|
|
2085
|
+
);
|
|
2086
|
+
} else {
|
|
2087
|
+
console.error("Error: no integration found. Run `cometchat apply` first.");
|
|
2088
|
+
}
|
|
2089
|
+
return 1;
|
|
2090
|
+
}
|
|
2091
|
+
const state = readState(root);
|
|
2092
|
+
if (!state) {
|
|
2093
|
+
return 1;
|
|
2094
|
+
}
|
|
2095
|
+
const checks = {
|
|
2096
|
+
css_variables_imported_once: checkCssVariablesImport(root),
|
|
2097
|
+
init_before_login: checkInitBeforeLogin(root),
|
|
2098
|
+
render_gated_on_login_resolve: checkRenderGatedOnLogin(root),
|
|
2099
|
+
no_auth_key_in_source: checkNoAuthKeyInSource(root, state.files_owned),
|
|
2100
|
+
error_ui_visible_on_failure: checkErrorUiVisible(root, state.files_owned)
|
|
2101
|
+
};
|
|
2102
|
+
const anyFailed = Object.values(checks).some((c) => c.status === "fail");
|
|
2103
|
+
const result = {
|
|
2104
|
+
status: anyFailed ? "fail" : "pass",
|
|
2105
|
+
checks
|
|
2106
|
+
};
|
|
2107
|
+
if (isJsonMode6(args)) {
|
|
2108
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2109
|
+
return anyFailed ? 1 : 0;
|
|
2110
|
+
}
|
|
2111
|
+
printHumanReadable5(result);
|
|
2112
|
+
return anyFailed ? 1 : 0;
|
|
2113
|
+
}
|
|
2114
|
+
function printHumanReadable5(r) {
|
|
2115
|
+
const lines = [];
|
|
2116
|
+
lines.push("");
|
|
2117
|
+
lines.push(` Verification: ${r.status === "pass" ? "\u2713 PASS" : "\u2717 FAIL"}`);
|
|
2118
|
+
lines.push("");
|
|
2119
|
+
for (const [name, check] of Object.entries(r.checks)) {
|
|
2120
|
+
const icon = check.status === "pass" ? "\u2713" : check.status === "fail" ? "\u2717" : "\u2014";
|
|
2121
|
+
lines.push(` ${icon} ${name}${check.reason ? ` (${check.reason})` : ""}`);
|
|
2122
|
+
if (check.details) lines.push(` ${check.details}`);
|
|
2123
|
+
}
|
|
2124
|
+
lines.push("");
|
|
2125
|
+
console.log(lines.join("\n"));
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
// src/commands/uninstall.ts
|
|
2129
|
+
import { rmSync as rmSync2, statSync as statSync3, rmdirSync, readdirSync as readdirSync3 } from "node:fs";
|
|
2130
|
+
import { dirname as dirname5, join as join7, resolve as resolve7 } from "node:path";
|
|
2131
|
+
function isJsonMode7(args) {
|
|
2132
|
+
return args.flags.json === true || args.flags.json === "true";
|
|
2133
|
+
}
|
|
2134
|
+
function isForceMode2(args) {
|
|
2135
|
+
return args.flags.force === true || args.flags.force === "true";
|
|
2136
|
+
}
|
|
2137
|
+
function projectPath6(args) {
|
|
2138
|
+
const fromFlag = args.flags.path;
|
|
2139
|
+
if (typeof fromFlag === "string") return resolve7(fromFlag);
|
|
2140
|
+
return resolve7(process.cwd());
|
|
2141
|
+
}
|
|
2142
|
+
function maybeRemoveEmptyDir(dir) {
|
|
2143
|
+
try {
|
|
2144
|
+
const entries = readdirSync3(dir);
|
|
2145
|
+
if (entries.length === 0) {
|
|
2146
|
+
rmdirSync(dir);
|
|
2147
|
+
maybeRemoveEmptyDir(dirname5(dir));
|
|
2148
|
+
}
|
|
2149
|
+
} catch {
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
async function uninstall(args) {
|
|
2153
|
+
const root = projectPath6(args);
|
|
2154
|
+
if (!hasState(root)) {
|
|
2155
|
+
const result2 = {
|
|
2156
|
+
status: "no-integration",
|
|
2157
|
+
files_removed: [],
|
|
2158
|
+
files_skipped: [],
|
|
2159
|
+
files_patched_left_alone: [],
|
|
2160
|
+
message: "No integration found in this project \u2014 nothing to uninstall."
|
|
2161
|
+
};
|
|
2162
|
+
return outputResult3(args, result2, 0);
|
|
2163
|
+
}
|
|
2164
|
+
const state = readState(root);
|
|
2165
|
+
if (!state) {
|
|
2166
|
+
if (isJsonMode7(args)) {
|
|
2167
|
+
console.log(JSON.stringify({ status: "error", error: "could not parse state file" }));
|
|
2168
|
+
} else {
|
|
2169
|
+
console.error("Error: could not parse .cometchat/state.json");
|
|
2170
|
+
}
|
|
2171
|
+
return 1;
|
|
2172
|
+
}
|
|
2173
|
+
const drift = detectDrift(root, state);
|
|
2174
|
+
if (drift.modified_files.length > 0 && !isForceMode2(args)) {
|
|
2175
|
+
const result2 = {
|
|
2176
|
+
status: "drift-detected",
|
|
2177
|
+
files_removed: [],
|
|
2178
|
+
files_skipped: drift.modified_files.map((path) => ({
|
|
2179
|
+
path,
|
|
2180
|
+
reason: "user-modified since apply (drift detected)"
|
|
2181
|
+
})),
|
|
2182
|
+
files_patched_left_alone: state.files_patched,
|
|
2183
|
+
message: `Refusing to uninstall: ${drift.modified_files.length} file(s) have been modified since apply. Run with --force to delete them anyway.`
|
|
2184
|
+
};
|
|
2185
|
+
return outputResult3(args, result2, 2);
|
|
2186
|
+
}
|
|
2187
|
+
const removed = [];
|
|
2188
|
+
const skipped = [];
|
|
2189
|
+
const dirsToCheck = /* @__PURE__ */ new Set();
|
|
2190
|
+
for (const file of state.files_owned) {
|
|
2191
|
+
const fullPath = join7(root, file);
|
|
2192
|
+
if (!pathExists(fullPath)) {
|
|
2193
|
+
skipped.push({ path: file, reason: "file not found" });
|
|
2194
|
+
continue;
|
|
2195
|
+
}
|
|
2196
|
+
try {
|
|
2197
|
+
rmSync2(fullPath);
|
|
2198
|
+
removed.push(file);
|
|
2199
|
+
dirsToCheck.add(dirname5(fullPath));
|
|
2200
|
+
} catch (err) {
|
|
2201
|
+
skipped.push({
|
|
2202
|
+
path: file,
|
|
2203
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
2204
|
+
});
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
for (const dir of dirsToCheck) {
|
|
2208
|
+
maybeRemoveEmptyDir(dir);
|
|
2209
|
+
}
|
|
2210
|
+
try {
|
|
2211
|
+
rmSync2(p(root, STATE_FILE_PATH));
|
|
2212
|
+
} catch {
|
|
2213
|
+
}
|
|
2214
|
+
try {
|
|
2215
|
+
const cometchatDir = p(root, ".cometchat");
|
|
2216
|
+
if (statSync3(cometchatDir).isDirectory()) {
|
|
2217
|
+
const entries = readdirSync3(cometchatDir);
|
|
2218
|
+
if (entries.length === 0) rmdirSync(cometchatDir);
|
|
2219
|
+
}
|
|
2220
|
+
} catch {
|
|
2221
|
+
}
|
|
2222
|
+
const result = {
|
|
2223
|
+
status: "uninstalled",
|
|
2224
|
+
files_removed: removed,
|
|
2225
|
+
files_skipped: skipped,
|
|
2226
|
+
files_patched_left_alone: state.files_patched,
|
|
2227
|
+
message: `Removed ${removed.length} file(s). Patches in ${state.files_patched.length} file(s) were left alone \u2014 clean up manually if needed.`
|
|
2228
|
+
};
|
|
2229
|
+
return outputResult3(args, result, 0);
|
|
2230
|
+
}
|
|
2231
|
+
function outputResult3(args, result, exitCode) {
|
|
2232
|
+
if (isJsonMode7(args)) {
|
|
2233
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2234
|
+
return exitCode;
|
|
2235
|
+
}
|
|
2236
|
+
printHumanReadable6(result);
|
|
2237
|
+
return exitCode;
|
|
2238
|
+
}
|
|
2239
|
+
function printHumanReadable6(r) {
|
|
2240
|
+
const lines = [];
|
|
2241
|
+
lines.push("");
|
|
2242
|
+
if (r.status === "no-integration") {
|
|
2243
|
+
lines.push(" " + r.message);
|
|
2244
|
+
lines.push("");
|
|
2245
|
+
console.log(lines.join("\n"));
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
if (r.status === "drift-detected") {
|
|
2249
|
+
lines.push(` \u26A0 ${r.message}`);
|
|
2250
|
+
lines.push("");
|
|
2251
|
+
if (r.files_skipped.length > 0) {
|
|
2252
|
+
lines.push(" Modified files:");
|
|
2253
|
+
for (const f of r.files_skipped) lines.push(` ~ ${f.path}`);
|
|
2254
|
+
}
|
|
2255
|
+
lines.push("");
|
|
2256
|
+
console.log(lines.join("\n"));
|
|
2257
|
+
return;
|
|
2258
|
+
}
|
|
2259
|
+
lines.push(` \u2713 ${r.message}`);
|
|
2260
|
+
lines.push("");
|
|
2261
|
+
if (r.files_removed.length > 0) {
|
|
2262
|
+
lines.push(` Removed files (${r.files_removed.length}):`);
|
|
2263
|
+
for (const f of r.files_removed) lines.push(` - ${f}`);
|
|
2264
|
+
}
|
|
2265
|
+
if (r.files_skipped.length > 0) {
|
|
2266
|
+
lines.push("");
|
|
2267
|
+
lines.push(` Skipped (${r.files_skipped.length}):`);
|
|
2268
|
+
for (const f of r.files_skipped) lines.push(` ! ${f.path} (${f.reason})`);
|
|
2269
|
+
}
|
|
2270
|
+
if (r.files_patched_left_alone.length > 0) {
|
|
2271
|
+
lines.push("");
|
|
2272
|
+
lines.push(` Patches left alone (${r.files_patched_left_alone.length}) \u2014 clean up manually if needed:`);
|
|
2273
|
+
for (const f of r.files_patched_left_alone) lines.push(` ? ${f.path} (${f.patch_id})`);
|
|
2274
|
+
}
|
|
2275
|
+
lines.push("");
|
|
2276
|
+
console.log(lines.join("\n"));
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
// src/commands/install.ts
|
|
2280
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
2281
|
+
import { resolve as resolve8 } from "node:path";
|
|
2282
|
+
function isJsonMode8(args) {
|
|
2283
|
+
return args.flags.json === true || args.flags.json === "true";
|
|
2284
|
+
}
|
|
2285
|
+
function projectPath7(args) {
|
|
2286
|
+
const fromFlag = args.flags.path;
|
|
2287
|
+
if (typeof fromFlag === "string") return resolve8(fromFlag);
|
|
2288
|
+
return resolve8(process.cwd());
|
|
2289
|
+
}
|
|
2290
|
+
function isDryRun(args) {
|
|
2291
|
+
return args.flags["dry-run"] === true;
|
|
2292
|
+
}
|
|
2293
|
+
function buildInstallCommand(pm, deps) {
|
|
2294
|
+
const runtime = deps.filter((d) => !d.dev).map((d) => `${d.name}@${d.version}`);
|
|
2295
|
+
const dev = deps.filter((d) => d.dev).map((d) => `${d.name}@${d.version}`);
|
|
2296
|
+
switch (pm) {
|
|
2297
|
+
case "pnpm":
|
|
2298
|
+
return { cmd: "pnpm", args: ["add", ...runtime, ...dev] };
|
|
2299
|
+
case "yarn":
|
|
2300
|
+
return { cmd: "yarn", args: ["add", ...runtime, ...dev] };
|
|
2301
|
+
case "bun":
|
|
2302
|
+
return { cmd: "bun", args: ["add", ...runtime, ...dev] };
|
|
2303
|
+
case "npm":
|
|
2304
|
+
default:
|
|
2305
|
+
return { cmd: "npm", args: ["install", ...runtime, ...dev] };
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
async function install(args) {
|
|
2309
|
+
const root = projectPath7(args);
|
|
2310
|
+
const sdkMajor = 6;
|
|
2311
|
+
if (!hasState(root)) {
|
|
2312
|
+
const result2 = {
|
|
2313
|
+
status: "no-integration",
|
|
2314
|
+
package_manager: "",
|
|
2315
|
+
command: "",
|
|
2316
|
+
deps_installed: [],
|
|
2317
|
+
duration_ms: 0,
|
|
2318
|
+
exit_code: 1,
|
|
2319
|
+
error: "No integration found. Run `cometchat apply` first."
|
|
2320
|
+
};
|
|
2321
|
+
return outputResult4(args, result2, 1);
|
|
2322
|
+
}
|
|
2323
|
+
const state = readState(root);
|
|
2324
|
+
if (!state) {
|
|
2325
|
+
if (isJsonMode8(args)) {
|
|
2326
|
+
console.log(JSON.stringify({ status: "error", error: "could not parse state file" }));
|
|
2327
|
+
} else {
|
|
2328
|
+
console.error("Error: could not parse .cometchat/state.json");
|
|
2329
|
+
}
|
|
2330
|
+
return 1;
|
|
2331
|
+
}
|
|
2332
|
+
let manifest;
|
|
2333
|
+
try {
|
|
2334
|
+
manifest = loadManifest(sdkMajor, state.framework, state.experience);
|
|
2335
|
+
} catch (err) {
|
|
2336
|
+
return errorOut2(
|
|
2337
|
+
args,
|
|
2338
|
+
"Error loading template manifest: " + (err instanceof Error ? err.message : String(err))
|
|
2339
|
+
);
|
|
2340
|
+
}
|
|
2341
|
+
if (!manifest.deps || manifest.deps.length === 0) {
|
|
2342
|
+
const result2 = {
|
|
2343
|
+
status: "no-deps",
|
|
2344
|
+
package_manager: "",
|
|
2345
|
+
command: "",
|
|
2346
|
+
deps_installed: [],
|
|
2347
|
+
duration_ms: 0,
|
|
2348
|
+
exit_code: 0
|
|
2349
|
+
};
|
|
2350
|
+
return outputResult4(args, result2, 0);
|
|
2351
|
+
}
|
|
2352
|
+
const pm = detectPackageManager(root) ?? "npm";
|
|
2353
|
+
const { cmd, args: cmdArgs } = buildInstallCommand(pm, manifest.deps);
|
|
2354
|
+
const fullCommand = `${cmd} ${cmdArgs.join(" ")}`;
|
|
2355
|
+
if (isDryRun(args)) {
|
|
2356
|
+
const result2 = {
|
|
2357
|
+
status: "installed",
|
|
2358
|
+
package_manager: pm,
|
|
2359
|
+
command: fullCommand,
|
|
2360
|
+
deps_installed: manifest.deps.map((d) => `${d.name}@${d.version}`),
|
|
2361
|
+
duration_ms: 0,
|
|
2362
|
+
exit_code: 0,
|
|
2363
|
+
stdout: "(dry run \u2014 no command executed)"
|
|
2364
|
+
};
|
|
2365
|
+
return outputResult4(args, result2, 0);
|
|
2366
|
+
}
|
|
2367
|
+
const start = Date.now();
|
|
2368
|
+
const proc = spawnSync2(cmd, cmdArgs, {
|
|
2369
|
+
cwd: root,
|
|
2370
|
+
stdio: isJsonMode8(args) ? ["ignore", "pipe", "pipe"] : "inherit",
|
|
2371
|
+
encoding: "utf8"
|
|
2372
|
+
});
|
|
2373
|
+
const duration = Date.now() - start;
|
|
2374
|
+
if (proc.error) {
|
|
2375
|
+
return errorOut2(
|
|
2376
|
+
args,
|
|
2377
|
+
`Failed to run ${cmd}: ${proc.error.message}. Is ${pm} installed?`
|
|
2378
|
+
);
|
|
2379
|
+
}
|
|
2380
|
+
const result = {
|
|
2381
|
+
status: proc.status === 0 ? "installed" : "error",
|
|
2382
|
+
package_manager: pm,
|
|
2383
|
+
command: fullCommand,
|
|
2384
|
+
deps_installed: proc.status === 0 ? manifest.deps.map((d) => `${d.name}@${d.version}`) : [],
|
|
2385
|
+
duration_ms: duration,
|
|
2386
|
+
exit_code: proc.status ?? -1,
|
|
2387
|
+
stdout: typeof proc.stdout === "string" ? proc.stdout : void 0,
|
|
2388
|
+
stderr: typeof proc.stderr === "string" ? proc.stderr : void 0,
|
|
2389
|
+
error: proc.status !== 0 ? `${pm} exited with code ${proc.status}` : void 0
|
|
2390
|
+
};
|
|
2391
|
+
return outputResult4(args, result, proc.status === 0 ? 0 : 1);
|
|
2392
|
+
}
|
|
2393
|
+
function outputResult4(args, result, exitCode) {
|
|
2394
|
+
if (isJsonMode8(args)) {
|
|
2395
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2396
|
+
return exitCode;
|
|
2397
|
+
}
|
|
2398
|
+
printHumanReadable7(result);
|
|
2399
|
+
return exitCode;
|
|
2400
|
+
}
|
|
2401
|
+
function errorOut2(args, message) {
|
|
2402
|
+
if (isJsonMode8(args)) {
|
|
2403
|
+
console.log(JSON.stringify({ status: "error", error: message }, null, 2));
|
|
2404
|
+
} else {
|
|
2405
|
+
console.error(message);
|
|
2406
|
+
}
|
|
2407
|
+
return 1;
|
|
2408
|
+
}
|
|
2409
|
+
function printHumanReadable7(r) {
|
|
2410
|
+
const lines = [];
|
|
2411
|
+
lines.push("");
|
|
2412
|
+
switch (r.status) {
|
|
2413
|
+
case "no-integration":
|
|
2414
|
+
lines.push(` \u2717 No integration found. Run \`cometchat apply\` first.`);
|
|
2415
|
+
break;
|
|
2416
|
+
case "no-deps":
|
|
2417
|
+
lines.push(` = No dependencies to install for this integration.`);
|
|
2418
|
+
break;
|
|
2419
|
+
case "installed":
|
|
2420
|
+
lines.push(` \u2713 Installed ${r.deps_installed.length} package(s) via ${r.package_manager}`);
|
|
2421
|
+
lines.push(` Command: ${r.command}`);
|
|
2422
|
+
lines.push(` Duration: ${r.duration_ms}ms`);
|
|
2423
|
+
if (r.deps_installed.length > 0) {
|
|
2424
|
+
lines.push(``);
|
|
2425
|
+
for (const d of r.deps_installed) lines.push(` + ${d}`);
|
|
2426
|
+
}
|
|
2427
|
+
break;
|
|
2428
|
+
case "error":
|
|
2429
|
+
lines.push(` \u2717 Install failed (${r.error ?? "unknown error"})`);
|
|
2430
|
+
lines.push(` Command: ${r.command}`);
|
|
2431
|
+
if (r.stderr) {
|
|
2432
|
+
lines.push(``);
|
|
2433
|
+
lines.push(` stderr:`);
|
|
2434
|
+
for (const line of r.stderr.split("\n").slice(0, 10)) {
|
|
2435
|
+
lines.push(` ${line}`);
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
break;
|
|
2439
|
+
}
|
|
2440
|
+
lines.push("");
|
|
2441
|
+
console.log(lines.join("\n"));
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
// src/commands/production-auth.ts
|
|
2445
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2446
|
+
import { join as join8, resolve as resolve9 } from "node:path";
|
|
2447
|
+
function isJsonMode9(args) {
|
|
2448
|
+
return args.flags.json === true || args.flags.json === "true";
|
|
2449
|
+
}
|
|
2450
|
+
function projectPath8(args) {
|
|
2451
|
+
const fromFlag = args.flags.path;
|
|
2452
|
+
if (typeof fromFlag === "string") return resolve9(fromFlag);
|
|
2453
|
+
return resolve9(process.cwd());
|
|
2454
|
+
}
|
|
2455
|
+
var SUPPORTED_FRAMEWORKS = /* @__PURE__ */ new Set(["nextjs", "react-router", "astro"]);
|
|
2456
|
+
function productionRoot() {
|
|
2457
|
+
return join8(findRegistryRoot(), "v6", "production");
|
|
2458
|
+
}
|
|
2459
|
+
function loadProductionManifest(framework) {
|
|
2460
|
+
const root = productionRoot();
|
|
2461
|
+
const manifestPath = join8(root, `${framework}.template.json`);
|
|
2462
|
+
const content = readFileSync7(manifestPath, "utf8");
|
|
2463
|
+
return JSON.parse(content);
|
|
2464
|
+
}
|
|
2465
|
+
function readProductionTemplate(framework, ref) {
|
|
2466
|
+
const root = productionRoot();
|
|
2467
|
+
return readFileSync7(join8(root, framework, ref), "utf8");
|
|
2468
|
+
}
|
|
2469
|
+
function findClientLoginFile(state) {
|
|
2470
|
+
const candidates = [
|
|
2471
|
+
"src/app/cometchat/CometChatNoSSR.tsx",
|
|
2472
|
+
// nextjs
|
|
2473
|
+
"app/cometchat/CometChatNoSSR.tsx",
|
|
2474
|
+
// react-router
|
|
2475
|
+
"src/cometchat/ChatApp.tsx"
|
|
2476
|
+
// astro
|
|
2477
|
+
];
|
|
2478
|
+
for (const c of candidates) {
|
|
2479
|
+
if (state.files_owned.includes(c)) return c;
|
|
2480
|
+
}
|
|
2481
|
+
return null;
|
|
2482
|
+
}
|
|
2483
|
+
var CANONICAL_LOGIN_RE = /(\s*)CometChatUIKit\.login\(\s*UID\s*\)\s*\n\s*\.then\(\s*setUser\s*\)\s*\n\s*\.catch\(\(\s*e\s*:\s*unknown\s*\)\s*=>\s*setError\(String\(e\)\)\s*\);?/;
|
|
2484
|
+
function rewriteLoginChain(content) {
|
|
2485
|
+
if (!CANONICAL_LOGIN_RE.test(content)) return null;
|
|
2486
|
+
return content.replace(
|
|
2487
|
+
CANONICAL_LOGIN_RE,
|
|
2488
|
+
(_match, indent) => `${indent}fetch("/api/cometchat-token?uid=" + encodeURIComponent(UID))
|
|
2489
|
+
${indent} .then((r) => r.json())
|
|
2490
|
+
${indent} .then((data) => CometChatUIKit.loginWithAuthToken(data.authToken))
|
|
2491
|
+
${indent} .then(setUser)
|
|
2492
|
+
${indent} .catch((e: unknown) => setError(String(e)));`
|
|
2493
|
+
);
|
|
2494
|
+
}
|
|
2495
|
+
function clientPatch(root, state) {
|
|
2496
|
+
const relPath = findClientLoginFile(state);
|
|
2497
|
+
if (relPath === null) return { status: "n/a" };
|
|
2498
|
+
const fullPath = join8(root, relPath);
|
|
2499
|
+
let content;
|
|
2500
|
+
try {
|
|
2501
|
+
content = readFileSync7(fullPath, "utf8");
|
|
2502
|
+
} catch {
|
|
2503
|
+
return { status: "skipped", file: relPath };
|
|
2504
|
+
}
|
|
2505
|
+
const next = rewriteLoginChain(content);
|
|
2506
|
+
if (next === null) return { status: "skipped", file: relPath };
|
|
2507
|
+
writeFileSync4(fullPath, next, "utf8");
|
|
2508
|
+
return { status: "applied", file: relPath, newChecksum: sha256(next) };
|
|
2509
|
+
}
|
|
2510
|
+
async function productionAuth(args) {
|
|
2511
|
+
const root = projectPath8(args);
|
|
2512
|
+
if (!hasState(root)) {
|
|
2513
|
+
const result2 = {
|
|
2514
|
+
status: "no-integration",
|
|
2515
|
+
framework: "",
|
|
2516
|
+
files_created: [],
|
|
2517
|
+
next_steps: [],
|
|
2518
|
+
error: "No integration found. Run `cometchat apply` first to create the dev integration, then run this command to upgrade to production-ready auth."
|
|
2519
|
+
};
|
|
2520
|
+
return outputResult5(args, result2, 1);
|
|
2521
|
+
}
|
|
2522
|
+
const state = readState(root);
|
|
2523
|
+
if (!state) {
|
|
2524
|
+
return errorOut3(args, "Could not parse .cometchat/state.json");
|
|
2525
|
+
}
|
|
2526
|
+
if (!SUPPORTED_FRAMEWORKS.has(state.framework)) {
|
|
2527
|
+
const result2 = {
|
|
2528
|
+
status: "unsupported-framework",
|
|
2529
|
+
framework: state.framework,
|
|
2530
|
+
files_created: [],
|
|
2531
|
+
next_steps: [
|
|
2532
|
+
`Production auth flow currently supports nextjs, react-router, and astro. Your integration is on ${state.framework}.`,
|
|
2533
|
+
"Reactjs (Vite/CRA) has no built-in server, so the token endpoint needs to live in a separate backend.",
|
|
2534
|
+
"Options:",
|
|
2535
|
+
" 1. Run a small Express/Hono/Bun server alongside your Vite app and create the endpoint there",
|
|
2536
|
+
" 2. Migrate to a framework with server support (Next.js, React Router framework mode, Astro SSR)",
|
|
2537
|
+
" 3. Use a serverless function (Vercel Edge, Cloudflare Workers, AWS Lambda)",
|
|
2538
|
+
"Whichever you pick, the endpoint logic is the same: POST to https://{APP_ID}.api-{REGION}.cometchat.io/v3/users/{uid}/auth_tokens with the apikey header (server-only env var) and return the authToken to the client."
|
|
2539
|
+
]
|
|
2540
|
+
};
|
|
2541
|
+
return outputResult5(args, result2, 2);
|
|
2542
|
+
}
|
|
2543
|
+
if (state.credentials_source === "token-endpoint") {
|
|
2544
|
+
const result2 = {
|
|
2545
|
+
status: "already-applied",
|
|
2546
|
+
framework: state.framework,
|
|
2547
|
+
files_created: [],
|
|
2548
|
+
next_steps: [
|
|
2549
|
+
"This integration is already using a server-side token endpoint.",
|
|
2550
|
+
"Run `cometchat info` to see current integration state."
|
|
2551
|
+
]
|
|
2552
|
+
};
|
|
2553
|
+
return outputResult5(args, result2, 0);
|
|
2554
|
+
}
|
|
2555
|
+
let manifest;
|
|
2556
|
+
try {
|
|
2557
|
+
manifest = loadProductionManifest(state.framework);
|
|
2558
|
+
} catch (err) {
|
|
2559
|
+
return errorOut3(args, "Could not load production manifest: " + (err instanceof Error ? err.message : String(err)));
|
|
2560
|
+
}
|
|
2561
|
+
const ops = [];
|
|
2562
|
+
const ownedPaths = [];
|
|
2563
|
+
for (const f of manifest.files) {
|
|
2564
|
+
if (f.action === "create") {
|
|
2565
|
+
if (!f.template_ref) continue;
|
|
2566
|
+
const content = readProductionTemplate(state.framework, f.template_ref);
|
|
2567
|
+
const owned = f.owned ?? true;
|
|
2568
|
+
ops.push({ type: "create", path: f.path, content, owned });
|
|
2569
|
+
if (owned) ownedPaths.push(f.path);
|
|
2570
|
+
} else if (f.action === "patch") {
|
|
2571
|
+
ops.push({
|
|
2572
|
+
type: "patch",
|
|
2573
|
+
path: f.path,
|
|
2574
|
+
patch_mode: f.patch_mode ?? "prepend",
|
|
2575
|
+
patch_content: f.patch_content ?? "",
|
|
2576
|
+
anchor: f.anchor,
|
|
2577
|
+
skip_if_exists: f.skip_if_exists,
|
|
2578
|
+
patch_id: `production/${state.framework}/${f.path}`
|
|
2579
|
+
});
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
let writeResult;
|
|
2583
|
+
try {
|
|
2584
|
+
writeResult = applyWrites(root, ops);
|
|
2585
|
+
} catch (err) {
|
|
2586
|
+
return errorOut3(args, "Production-auth apply failed: " + (err instanceof Error ? err.message : String(err)));
|
|
2587
|
+
}
|
|
2588
|
+
const patchResult = clientPatch(root, state);
|
|
2589
|
+
const newChecksums = { ...state.checksums, ...writeResult.checksums };
|
|
2590
|
+
if (patchResult.status === "applied" && patchResult.file && patchResult.newChecksum) {
|
|
2591
|
+
newChecksums[patchResult.file] = patchResult.newChecksum;
|
|
2592
|
+
}
|
|
2593
|
+
const updatedState = {
|
|
2594
|
+
...state,
|
|
2595
|
+
credentials_source: "token-endpoint",
|
|
2596
|
+
files_owned: [...state.files_owned, ...ownedPaths],
|
|
2597
|
+
checksums: newChecksums
|
|
2598
|
+
};
|
|
2599
|
+
writeState(root, updatedState);
|
|
2600
|
+
const next_steps = buildNextSteps(state.framework, manifest.next_steps, patchResult);
|
|
2601
|
+
const result = {
|
|
2602
|
+
status: "applied",
|
|
2603
|
+
framework: state.framework,
|
|
2604
|
+
files_created: writeResult.files_created,
|
|
2605
|
+
files_patched: patchResult.status === "applied" && patchResult.file ? [patchResult.file] : [],
|
|
2606
|
+
client_patch: patchResult.status,
|
|
2607
|
+
next_steps
|
|
2608
|
+
};
|
|
2609
|
+
appendAuditEntry(root, {
|
|
2610
|
+
command: "production-auth",
|
|
2611
|
+
summary: `Upgraded ${state.framework} integration from dev (Auth Key) to production (server-side token endpoint)${patchResult.status === "applied" ? " + auto-patched the client login flow" : patchResult.status === "skipped" ? " (client login auto-patch skipped \u2014 file was customized)" : ""}`,
|
|
2612
|
+
inputs: {
|
|
2613
|
+
framework: state.framework,
|
|
2614
|
+
...state.router ? { router: state.router } : {}
|
|
2615
|
+
},
|
|
2616
|
+
decisions: {
|
|
2617
|
+
endpoint_path: `picked from production manifest for ${state.framework} \u2014 server-side route that proxies to https://{APP_ID}.api-{REGION}.cometchat.io/v3/users/{uid}/auth_tokens with the apikey header`,
|
|
2618
|
+
auth_key_handling: `the AUTH_KEY env var is now server-only \u2014 clients receive short-lived auth tokens instead of the long-lived key. .env update is up to the user (the CLI never reads/writes secrets).`,
|
|
2619
|
+
client_patch: patchResult.status === "applied" ? `auto-rewrote the canonical CometChatUIKit.login(UID).then(setUser) chain in ${patchResult.file ?? "the client file"} to fetch('/api/cometchat-token?uid=' + UID).then(loginWithAuthToken).then(setUser). The pattern matched, so the rewrite is safe.` : patchResult.status === "skipped" ? `the canonical login pattern wasn't found (the client file was customized) \u2014 the manual fallback steps in next_actions are the safety net` : `no candidate client file in state.files_owned`
|
|
2620
|
+
},
|
|
2621
|
+
files_created: writeResult.files_created,
|
|
2622
|
+
files_modified: result.files_patched && result.files_patched.length > 0 ? result.files_patched : void 0,
|
|
2623
|
+
next_actions: result.next_steps
|
|
2624
|
+
});
|
|
2625
|
+
return outputResult5(args, result, 0);
|
|
2626
|
+
}
|
|
2627
|
+
function buildNextSteps(_framework, manifestSteps, patchResult) {
|
|
2628
|
+
if (patchResult.status === "applied" && patchResult.file) {
|
|
2629
|
+
const out = [];
|
|
2630
|
+
out.push(`\u2713 Client login flow auto-patched in ${patchResult.file}.`);
|
|
2631
|
+
out.push(" CometChatUIKit.login(UID) \u2192 fetch('/api/cometchat-token?uid=...').then(loginWithAuthToken)");
|
|
2632
|
+
out.push("");
|
|
2633
|
+
let skipping = false;
|
|
2634
|
+
for (const step of manifestSteps) {
|
|
2635
|
+
if (skipping) {
|
|
2636
|
+
if (/^\d+\./.test(step) && !step.includes("login flow:") && !step.includes("CometChatNoSSR") && !step.includes("ChatApp")) {
|
|
2637
|
+
skipping = false;
|
|
2638
|
+
out.push(step);
|
|
2639
|
+
continue;
|
|
2640
|
+
}
|
|
2641
|
+
continue;
|
|
2642
|
+
}
|
|
2643
|
+
if (/^\d+\.\s+In\s+\S+(CometChatNoSSR|ChatApp)\.tsx,?\s+replace the login flow:?$/.test(step) || // Belt-and-suspenders for slightly different phrasings
|
|
2644
|
+
/^\d+\./.test(step) && step.includes("replace the login flow")) {
|
|
2645
|
+
skipping = true;
|
|
2646
|
+
continue;
|
|
2647
|
+
}
|
|
2648
|
+
out.push(step);
|
|
2649
|
+
}
|
|
2650
|
+
out.push("");
|
|
2651
|
+
out.push("\u26A0 The hardcoded UID constant in the patched file still points at 'cometchat-uid-1'.");
|
|
2652
|
+
out.push(" Replace it with your real authenticated user's ID (from NextAuth/Clerk/your session).");
|
|
2653
|
+
return out;
|
|
2654
|
+
}
|
|
2655
|
+
if (patchResult.status === "skipped" && patchResult.file) {
|
|
2656
|
+
return [
|
|
2657
|
+
`\u26A0 Auto-patch SKIPPED for ${patchResult.file}: the canonical login pattern wasn't found, likely because you customized that file.`,
|
|
2658
|
+
" Falling back to manual instructions:",
|
|
2659
|
+
"",
|
|
2660
|
+
...manifestSteps
|
|
2661
|
+
];
|
|
2662
|
+
}
|
|
2663
|
+
return manifestSteps;
|
|
2664
|
+
}
|
|
2665
|
+
function outputResult5(args, result, exitCode) {
|
|
2666
|
+
if (isJsonMode9(args)) {
|
|
2667
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2668
|
+
return exitCode;
|
|
2669
|
+
}
|
|
2670
|
+
printHumanReadable8(result);
|
|
2671
|
+
return exitCode;
|
|
2672
|
+
}
|
|
2673
|
+
function errorOut3(args, message) {
|
|
2674
|
+
if (isJsonMode9(args)) {
|
|
2675
|
+
console.log(JSON.stringify({ status: "error", error: message }, null, 2));
|
|
2676
|
+
} else {
|
|
2677
|
+
console.error(message);
|
|
2678
|
+
}
|
|
2679
|
+
return 1;
|
|
2680
|
+
}
|
|
2681
|
+
function printHumanReadable8(r) {
|
|
2682
|
+
const lines = [];
|
|
2683
|
+
lines.push("");
|
|
2684
|
+
switch (r.status) {
|
|
2685
|
+
case "no-integration":
|
|
2686
|
+
lines.push(` \u2717 ${r.error}`);
|
|
2687
|
+
break;
|
|
2688
|
+
case "unsupported-framework":
|
|
2689
|
+
lines.push(` \u26A0 Framework "${r.framework}" not yet supported for production-auth (Phase 5: nextjs only).`);
|
|
2690
|
+
lines.push("");
|
|
2691
|
+
for (const step of r.next_steps) lines.push(` ${step}`);
|
|
2692
|
+
break;
|
|
2693
|
+
case "already-applied":
|
|
2694
|
+
lines.push(` = ${r.next_steps[0] ?? "Already on production auth."}`);
|
|
2695
|
+
break;
|
|
2696
|
+
case "applied":
|
|
2697
|
+
lines.push(` \u2713 Upgraded to production auth (${r.framework})`);
|
|
2698
|
+
lines.push("");
|
|
2699
|
+
lines.push(` Files created (${r.files_created.length}):`);
|
|
2700
|
+
for (const f of r.files_created) lines.push(` + ${f}`);
|
|
2701
|
+
if (r.files_patched && r.files_patched.length > 0) {
|
|
2702
|
+
lines.push("");
|
|
2703
|
+
lines.push(` Files patched (${r.files_patched.length}):`);
|
|
2704
|
+
for (const f of r.files_patched) lines.push(` ~ ${f}`);
|
|
2705
|
+
}
|
|
2706
|
+
lines.push("");
|
|
2707
|
+
if (r.client_patch === "applied") {
|
|
2708
|
+
lines.push(" \u2713 Client login flow auto-rewritten \u2014 env-var + restart-server steps still need you:");
|
|
2709
|
+
} else if (r.client_patch === "skipped") {
|
|
2710
|
+
lines.push(" \u26A0 Auto-patch SKIPPED \u2014 your client file was customized. Apply manually:");
|
|
2711
|
+
} else {
|
|
2712
|
+
lines.push(" \u26A0 MANUAL CLIENT-SIDE CHANGES REQUIRED:");
|
|
2713
|
+
}
|
|
2714
|
+
lines.push("");
|
|
2715
|
+
for (const step of r.next_steps) lines.push(` ${step}`);
|
|
2716
|
+
break;
|
|
2717
|
+
case "error":
|
|
2718
|
+
lines.push(` \u2717 ${r.error ?? "Unknown error"}`);
|
|
2719
|
+
break;
|
|
2720
|
+
}
|
|
2721
|
+
lines.push("");
|
|
2722
|
+
console.log(lines.join("\n"));
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
// src/commands/apply-theme.ts
|
|
2726
|
+
import { resolve as resolve10 } from "node:path";
|
|
2727
|
+
function isJsonMode10(args) {
|
|
2728
|
+
return args.flags.json === true || args.flags.json === "true";
|
|
2729
|
+
}
|
|
2730
|
+
function projectPath9(args) {
|
|
2731
|
+
const fromFlag = args.flags.path;
|
|
2732
|
+
if (typeof fromFlag === "string") return resolve10(fromFlag);
|
|
2733
|
+
return resolve10(process.cwd());
|
|
2734
|
+
}
|
|
2735
|
+
function getStringFlag3(args, name) {
|
|
2736
|
+
const v = args.flags[name];
|
|
2737
|
+
return typeof v === "string" ? v : null;
|
|
2738
|
+
}
|
|
2739
|
+
var THEME_PRESETS = {
|
|
2740
|
+
slack: {
|
|
2741
|
+
primaryColor: "#611f69",
|
|
2742
|
+
textColor: "#1d1c1d",
|
|
2743
|
+
backgroundColor: "#ffffff",
|
|
2744
|
+
fontFamily: "Lato, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
|
2745
|
+
borderRadius: "8px",
|
|
2746
|
+
darkMode: false
|
|
2747
|
+
},
|
|
2748
|
+
whatsapp: {
|
|
2749
|
+
primaryColor: "#25d366",
|
|
2750
|
+
textColor: "#111b21",
|
|
2751
|
+
backgroundColor: "#f0f2f5",
|
|
2752
|
+
fontFamily: "'Segoe UI', Helvetica, Arial, sans-serif",
|
|
2753
|
+
borderRadius: "12px",
|
|
2754
|
+
darkMode: false
|
|
2755
|
+
},
|
|
2756
|
+
imessage: {
|
|
2757
|
+
primaryColor: "#007aff",
|
|
2758
|
+
textColor: "#000000",
|
|
2759
|
+
backgroundColor: "#ffffff",
|
|
2760
|
+
fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', sans-serif",
|
|
2761
|
+
borderRadius: "18px",
|
|
2762
|
+
darkMode: false
|
|
2763
|
+
},
|
|
2764
|
+
discord: {
|
|
2765
|
+
primaryColor: "#5865f2",
|
|
2766
|
+
textColor: "#dcddde",
|
|
2767
|
+
backgroundColor: "#36393f",
|
|
2768
|
+
fontFamily: "'gg sans', 'Noto Sans', Helvetica, Arial, sans-serif",
|
|
2769
|
+
borderRadius: "8px",
|
|
2770
|
+
darkMode: true
|
|
2771
|
+
},
|
|
2772
|
+
notion: {
|
|
2773
|
+
primaryColor: "#2eaadc",
|
|
2774
|
+
textColor: "#37352f",
|
|
2775
|
+
backgroundColor: "#ffffff",
|
|
2776
|
+
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, 'Apple Color Emoji', Arial, sans-serif",
|
|
2777
|
+
borderRadius: "6px",
|
|
2778
|
+
darkMode: false
|
|
2779
|
+
}
|
|
2780
|
+
};
|
|
2781
|
+
function listPresets() {
|
|
2782
|
+
return Object.keys(THEME_PRESETS);
|
|
2783
|
+
}
|
|
2784
|
+
function parseThemeInput(args) {
|
|
2785
|
+
const preset = getStringFlag3(args, "preset");
|
|
2786
|
+
if (preset) {
|
|
2787
|
+
const base = THEME_PRESETS[preset.toLowerCase()];
|
|
2788
|
+
if (!base) {
|
|
2789
|
+
return null;
|
|
2790
|
+
}
|
|
2791
|
+
return {
|
|
2792
|
+
primaryColor: getStringFlag3(args, "primary-color") ?? base.primaryColor,
|
|
2793
|
+
textColor: getStringFlag3(args, "text-color") ?? base.textColor,
|
|
2794
|
+
backgroundColor: getStringFlag3(args, "background-color") ?? base.backgroundColor,
|
|
2795
|
+
fontFamily: getStringFlag3(args, "font-family") ?? base.fontFamily,
|
|
2796
|
+
borderRadius: getStringFlag3(args, "border-radius") ?? base.borderRadius,
|
|
2797
|
+
darkMode: args.flags["dark-mode"] === true || args.flags["dark-mode"] === "true" ? true : base.darkMode
|
|
2798
|
+
};
|
|
2799
|
+
}
|
|
2800
|
+
const primary = getStringFlag3(args, "primary-color");
|
|
2801
|
+
if (!primary) return null;
|
|
2802
|
+
return {
|
|
2803
|
+
primaryColor: primary,
|
|
2804
|
+
textColor: getStringFlag3(args, "text-color") ?? void 0,
|
|
2805
|
+
backgroundColor: getStringFlag3(args, "background-color") ?? void 0,
|
|
2806
|
+
fontFamily: getStringFlag3(args, "font-family") ?? void 0,
|
|
2807
|
+
borderRadius: getStringFlag3(args, "border-radius") ?? void 0,
|
|
2808
|
+
darkMode: args.flags["dark-mode"] === true || args.flags["dark-mode"] === "true"
|
|
2809
|
+
};
|
|
2810
|
+
}
|
|
2811
|
+
function buildOverrideBlock(theme) {
|
|
2812
|
+
const lines = [];
|
|
2813
|
+
const vars = [];
|
|
2814
|
+
lines.push("/* CometChat theme overrides \u2014 generated by `cometchat apply-theme` */");
|
|
2815
|
+
lines.push(":root {");
|
|
2816
|
+
lines.push(` --cometchat-primary-color: ${theme.primaryColor};`);
|
|
2817
|
+
vars.push("--cometchat-primary-color");
|
|
2818
|
+
if (theme.textColor) {
|
|
2819
|
+
lines.push(` --cometchat-text-color-primary: ${theme.textColor};`);
|
|
2820
|
+
vars.push("--cometchat-text-color-primary");
|
|
2821
|
+
}
|
|
2822
|
+
if (theme.backgroundColor) {
|
|
2823
|
+
lines.push(` --cometchat-background-color-01: ${theme.backgroundColor};`);
|
|
2824
|
+
vars.push("--cometchat-background-color-01");
|
|
2825
|
+
}
|
|
2826
|
+
if (theme.fontFamily) {
|
|
2827
|
+
lines.push(` --cometchat-font-family: ${theme.fontFamily};`);
|
|
2828
|
+
vars.push("--cometchat-font-family");
|
|
2829
|
+
}
|
|
2830
|
+
if (theme.borderRadius) {
|
|
2831
|
+
lines.push(` --cometchat-radius-3: ${theme.borderRadius};`);
|
|
2832
|
+
vars.push("--cometchat-radius-3");
|
|
2833
|
+
}
|
|
2834
|
+
lines.push("}");
|
|
2835
|
+
if (theme.darkMode) {
|
|
2836
|
+
lines.push("");
|
|
2837
|
+
lines.push("@media (prefers-color-scheme: dark) {");
|
|
2838
|
+
lines.push(" :root {");
|
|
2839
|
+
lines.push(` --cometchat-background-color-01: #0a0a0a;`);
|
|
2840
|
+
lines.push(` --cometchat-text-color-primary: #ededed;`);
|
|
2841
|
+
lines.push(` --cometchat-background-color-02: #1a1a1a;`);
|
|
2842
|
+
lines.push(` --cometchat-background-color-03: #2a2a2a;`);
|
|
2843
|
+
lines.push(" }");
|
|
2844
|
+
lines.push("}");
|
|
2845
|
+
vars.push(
|
|
2846
|
+
"--cometchat-background-color-01 (dark)",
|
|
2847
|
+
"--cometchat-text-color-primary (dark)",
|
|
2848
|
+
"--cometchat-background-color-02 (dark)",
|
|
2849
|
+
"--cometchat-background-color-03 (dark)"
|
|
2850
|
+
);
|
|
2851
|
+
}
|
|
2852
|
+
return { css: lines.join("\n") + "\n", variables: vars };
|
|
2853
|
+
}
|
|
2854
|
+
function buildThemeOp(framework, cssBlock, root) {
|
|
2855
|
+
const targets = {
|
|
2856
|
+
reactjs: ["src/index.css", "src/main.css", "src/styles.css"],
|
|
2857
|
+
nextjs: ["src/app/globals.css", "app/globals.css"],
|
|
2858
|
+
"react-router": ["app/app.css", "app/styles/app.css"],
|
|
2859
|
+
astro: ["src/cometchat/ChatApp.tsx"]
|
|
2860
|
+
// Special!
|
|
2861
|
+
};
|
|
2862
|
+
const candidates = targets[framework] ?? [];
|
|
2863
|
+
for (const candidate of candidates) {
|
|
2864
|
+
if (pathExists(p(root, candidate))) {
|
|
2865
|
+
if (framework === "astro") {
|
|
2866
|
+
return {
|
|
2867
|
+
op: {
|
|
2868
|
+
type: "patch",
|
|
2869
|
+
path: candidate,
|
|
2870
|
+
patch_mode: "insert-after",
|
|
2871
|
+
anchor: 'import "@cometchat/chat-uikit-react/css-variables.css";',
|
|
2872
|
+
patch_content: `
|
|
2873
|
+
// CometChat theme overrides \u2014 generated by \`cometchat apply-theme\`
|
|
2874
|
+
// This must live inside the React island .tsx file (not a global stylesheet)
|
|
2875
|
+
// because Astro's client:only islands don't pick up global CSS.
|
|
2876
|
+
// Wrap the CSS in a <style> tag mounted via useEffect, OR use inline style on the root div:
|
|
2877
|
+
/*
|
|
2878
|
+
${cssBlock.split("\n").map((l) => " " + l).join("\n")}
|
|
2879
|
+
*/`,
|
|
2880
|
+
skip_if_exists: "/* CometChat theme overrides",
|
|
2881
|
+
patch_id: "theme/astro"
|
|
2882
|
+
},
|
|
2883
|
+
targetPath: candidate,
|
|
2884
|
+
specialHint: "Astro theming requires extra steps: the override block was inserted as a comment in ChatApp.tsx. Copy it into a CSS file imported by the React island, OR pass it as inline styles on the wrapper element. Future versions of cometchat apply-theme will automate this."
|
|
2885
|
+
};
|
|
2886
|
+
}
|
|
2887
|
+
return {
|
|
2888
|
+
op: {
|
|
2889
|
+
type: "patch",
|
|
2890
|
+
path: candidate,
|
|
2891
|
+
patch_mode: "append",
|
|
2892
|
+
patch_content: "\n" + cssBlock,
|
|
2893
|
+
skip_if_exists: "/* CometChat theme overrides",
|
|
2894
|
+
patch_id: `theme/${framework}`
|
|
2895
|
+
},
|
|
2896
|
+
targetPath: candidate
|
|
2897
|
+
};
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
return { op: null, targetPath: null };
|
|
2901
|
+
}
|
|
2902
|
+
async function applyTheme(args) {
|
|
2903
|
+
const root = projectPath9(args);
|
|
2904
|
+
if (!hasState(root)) {
|
|
2905
|
+
return outputResult6(
|
|
2906
|
+
args,
|
|
2907
|
+
{
|
|
2908
|
+
status: "no-integration",
|
|
2909
|
+
framework: "",
|
|
2910
|
+
file_modified: null,
|
|
2911
|
+
variables_applied: [],
|
|
2912
|
+
next_steps: [],
|
|
2913
|
+
error: "No integration found. Run `cometchat apply` first, then `cometchat apply-theme` to customize the look."
|
|
2914
|
+
},
|
|
2915
|
+
1
|
|
2916
|
+
);
|
|
2917
|
+
}
|
|
2918
|
+
const state = readState(root);
|
|
2919
|
+
if (!state) {
|
|
2920
|
+
return errorOut4(args, "Could not parse .cometchat/state.json");
|
|
2921
|
+
}
|
|
2922
|
+
const theme = parseThemeInput(args);
|
|
2923
|
+
if (!theme) {
|
|
2924
|
+
const requestedPreset = getStringFlag3(args, "preset");
|
|
2925
|
+
if (requestedPreset) {
|
|
2926
|
+
return errorOut4(
|
|
2927
|
+
args,
|
|
2928
|
+
`Error: unknown preset "${requestedPreset}". Available presets: ${listPresets().join(", ")}.`
|
|
2929
|
+
);
|
|
2930
|
+
}
|
|
2931
|
+
return errorOut4(
|
|
2932
|
+
args,
|
|
2933
|
+
`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.`
|
|
2934
|
+
);
|
|
2935
|
+
}
|
|
2936
|
+
const { css, variables } = buildOverrideBlock(theme);
|
|
2937
|
+
const { op, targetPath, specialHint } = buildThemeOp(state.framework, css, root);
|
|
2938
|
+
if (!op || !targetPath) {
|
|
2939
|
+
return outputResult6(
|
|
2940
|
+
args,
|
|
2941
|
+
{
|
|
2942
|
+
status: "no-target-file",
|
|
2943
|
+
framework: state.framework,
|
|
2944
|
+
file_modified: null,
|
|
2945
|
+
variables_applied: [],
|
|
2946
|
+
next_steps: [
|
|
2947
|
+
`Could not find a CSS file to write theme overrides to for framework ${state.framework}.`,
|
|
2948
|
+
"Expected one of:",
|
|
2949
|
+
state.framework === "reactjs" ? " src/index.css" : state.framework === "nextjs" ? " src/app/globals.css or app/globals.css" : state.framework === "react-router" ? " app/app.css" : " (unknown framework)"
|
|
2950
|
+
]
|
|
2951
|
+
},
|
|
2952
|
+
1
|
|
2953
|
+
);
|
|
2954
|
+
}
|
|
2955
|
+
let writeResult;
|
|
2956
|
+
try {
|
|
2957
|
+
writeResult = applyWrites(root, [op]);
|
|
2958
|
+
} catch (err) {
|
|
2959
|
+
return errorOut4(args, "Theme apply failed: " + (err instanceof Error ? err.message : String(err)));
|
|
2960
|
+
}
|
|
2961
|
+
const wasSkipped = writeResult.files_skipped.length > 0;
|
|
2962
|
+
const result = {
|
|
2963
|
+
status: "applied",
|
|
2964
|
+
framework: state.framework,
|
|
2965
|
+
file_modified: targetPath,
|
|
2966
|
+
variables_applied: variables,
|
|
2967
|
+
next_steps: [
|
|
2968
|
+
wasSkipped ? `Skipped: ${targetPath} already contains a CometChat theme override block. Remove the existing block to re-apply.` : `Theme overrides written to ${targetPath}.`,
|
|
2969
|
+
...specialHint ? [specialHint] : [],
|
|
2970
|
+
"Restart your dev server (`npm run dev`) to see the new theme.",
|
|
2971
|
+
"To remove the theme, delete the /* CometChat theme overrides */ block from the file."
|
|
2972
|
+
]
|
|
2973
|
+
};
|
|
2974
|
+
if (!wasSkipped) {
|
|
2975
|
+
const presetFlag = getStringFlag3(args, "preset");
|
|
2976
|
+
appendAuditEntry(root, {
|
|
2977
|
+
command: "apply-theme",
|
|
2978
|
+
summary: presetFlag ? `Applied "${presetFlag}" theme preset to ${state.framework} integration` : `Applied custom theme overrides to ${state.framework} integration`,
|
|
2979
|
+
inputs: {
|
|
2980
|
+
framework: state.framework,
|
|
2981
|
+
...presetFlag ? { preset: presetFlag } : {},
|
|
2982
|
+
...theme.primaryColor ? { "primary-color": theme.primaryColor } : {},
|
|
2983
|
+
...theme.darkMode ? { "dark-mode": true } : {}
|
|
2984
|
+
},
|
|
2985
|
+
decisions: {
|
|
2986
|
+
css_target: state.framework === "astro" ? `Astro: theme block inserted as a comment in src/cometchat/ChatApp.tsx (the React island), because Astro's client:only="react" islands don't pick up global stylesheets` : `${state.framework}: theme block appended to ${targetPath} (the global stylesheet that the integration's css-variables.css import lives in)`,
|
|
2987
|
+
...presetFlag ? {
|
|
2988
|
+
preset: `picked the "${presetFlag}" preset bundle (primary + text + background + font + radius + dark-mode in one shot); any individual flags passed alongside override the preset's value`
|
|
2989
|
+
} : {},
|
|
2990
|
+
variables_set: variables.join(", ")
|
|
2991
|
+
},
|
|
2992
|
+
files_patched: [targetPath],
|
|
2993
|
+
next_actions: result.next_steps
|
|
2994
|
+
});
|
|
2995
|
+
}
|
|
2996
|
+
return outputResult6(args, result, 0);
|
|
2997
|
+
}
|
|
2998
|
+
function outputResult6(args, result, exitCode) {
|
|
2999
|
+
if (isJsonMode10(args)) {
|
|
3000
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3001
|
+
return exitCode;
|
|
3002
|
+
}
|
|
3003
|
+
printHumanReadable9(result);
|
|
3004
|
+
return exitCode;
|
|
3005
|
+
}
|
|
3006
|
+
function errorOut4(args, message) {
|
|
3007
|
+
if (isJsonMode10(args)) {
|
|
3008
|
+
console.log(JSON.stringify({ status: "error", error: message }, null, 2));
|
|
3009
|
+
} else {
|
|
3010
|
+
console.error(message);
|
|
3011
|
+
}
|
|
3012
|
+
return 1;
|
|
3013
|
+
}
|
|
3014
|
+
function printHumanReadable9(r) {
|
|
3015
|
+
const lines = [];
|
|
3016
|
+
lines.push("");
|
|
3017
|
+
switch (r.status) {
|
|
3018
|
+
case "no-integration":
|
|
3019
|
+
lines.push(` \u2717 ${r.error}`);
|
|
3020
|
+
break;
|
|
3021
|
+
case "no-target-file":
|
|
3022
|
+
lines.push(` \u2717 ${r.next_steps[0] ?? "No target CSS file found"}`);
|
|
3023
|
+
for (const step of r.next_steps.slice(1)) lines.push(` ${step}`);
|
|
3024
|
+
break;
|
|
3025
|
+
case "applied":
|
|
3026
|
+
lines.push(` \u2713 Theme applied to ${r.framework}`);
|
|
3027
|
+
if (r.file_modified) lines.push(` File: ${r.file_modified}`);
|
|
3028
|
+
lines.push("");
|
|
3029
|
+
lines.push(` CSS variables overridden (${r.variables_applied.length}):`);
|
|
3030
|
+
for (const v of r.variables_applied) lines.push(` + ${v}`);
|
|
3031
|
+
lines.push("");
|
|
3032
|
+
lines.push(" Next steps:");
|
|
3033
|
+
for (const step of r.next_steps) lines.push(` - ${step}`);
|
|
3034
|
+
break;
|
|
3035
|
+
case "error":
|
|
3036
|
+
lines.push(` \u2717 ${r.error ?? "Unknown error"}`);
|
|
3037
|
+
break;
|
|
3038
|
+
}
|
|
3039
|
+
lines.push("");
|
|
3040
|
+
console.log(lines.join("\n"));
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
// src/commands/features.ts
|
|
3044
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
3045
|
+
import { join as join9 } from "node:path";
|
|
3046
|
+
function isJsonMode11(args) {
|
|
3047
|
+
return args.flags.json === true || args.flags.json === "true";
|
|
3048
|
+
}
|
|
3049
|
+
function catalogPath() {
|
|
3050
|
+
return join9(findRegistryRoot(), "v6", "features", "catalog.json");
|
|
3051
|
+
}
|
|
3052
|
+
var cachedCatalog = null;
|
|
3053
|
+
function loadCatalog() {
|
|
3054
|
+
if (cachedCatalog) return cachedCatalog;
|
|
3055
|
+
const path = catalogPath();
|
|
3056
|
+
cachedCatalog = JSON.parse(readFileSync8(path, "utf8"));
|
|
3057
|
+
return cachedCatalog;
|
|
3058
|
+
}
|
|
3059
|
+
function nextStepsForFeature(feature) {
|
|
3060
|
+
switch (feature.type) {
|
|
3061
|
+
case "default":
|
|
3062
|
+
return [
|
|
3063
|
+
`${feature.name} is already enabled in the CometChat UI Kit components your integration uses.`,
|
|
3064
|
+
...feature.components && feature.components.length > 0 ? [`It's rendered by: ${feature.components.join(", ")}`] : [],
|
|
3065
|
+
feature.requires_dashboard_setup ? "Some features (like moderation) need rules configured in the CometChat dashboard before they activate. Check the dashboard." : "No code changes needed \u2014 it's already there.",
|
|
3066
|
+
...feature.docs_topic ? [`For details, query the docs MCP for "${feature.docs_topic}" or visit https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}`] : []
|
|
3067
|
+
];
|
|
3068
|
+
case "dashboard-toggle": {
|
|
3069
|
+
const lines = [];
|
|
3070
|
+
if (feature.auto_wired_in_uikit) {
|
|
3071
|
+
lines.push(
|
|
3072
|
+
`${feature.name} is in the UI Kit's defaultExtensions[] \u2014 its UI decorator is attached automatically by initiateAfterLogin(). The only thing you need to do is flip the dashboard toggle.`,
|
|
3073
|
+
"",
|
|
3074
|
+
"Steps to enable:",
|
|
3075
|
+
` 1. Go to https://app.cometchat.com and select your app`,
|
|
3076
|
+
` 2. Navigate to: ${feature.dashboard_path ?? "Extensions \u2192 " + feature.name}`,
|
|
3077
|
+
` 3. Toggle the feature on (and configure any required values)`,
|
|
3078
|
+
` 4. Refresh your dev server \u2014 no code changes required`
|
|
3079
|
+
);
|
|
3080
|
+
} else {
|
|
3081
|
+
lines.push(
|
|
3082
|
+
`${feature.name} is a dashboard-toggle extension that ALSO requires explicit registration in your UIKitSettingsBuilder. The UI Kit only auto-attaches the 7 extensions in defaultExtensions[]; this one is not in that list.`,
|
|
3083
|
+
"",
|
|
3084
|
+
"Steps to enable:",
|
|
3085
|
+
` 1. Go to https://app.cometchat.com \u2192 ${feature.dashboard_path ?? "Extensions \u2192 " + feature.name}, toggle on`,
|
|
3086
|
+
` 2. In your CometChat init (the file that calls UIKitSettingsBuilder), add the extension:`,
|
|
3087
|
+
` import { CometChatUIKit, UIKitSettingsBuilder } from "@cometchat/chat-uikit-react";`,
|
|
3088
|
+
` // Pseudocode \u2014 match your existing builder chain:`,
|
|
3089
|
+
` const settings = new UIKitSettingsBuilder()`,
|
|
3090
|
+
` .setAppId(APP_ID)`,
|
|
3091
|
+
` .setRegion(REGION)`,
|
|
3092
|
+
` .setAuthKey(AUTH_KEY)`,
|
|
3093
|
+
` .subscribePresenceForAllUsers()`,
|
|
3094
|
+
` .setExtensions([/* your extension instance here */])`,
|
|
3095
|
+
` .build();`,
|
|
3096
|
+
` CometChatUIKit.init(settings);`,
|
|
3097
|
+
` 3. Restart your dev server`
|
|
3098
|
+
);
|
|
3099
|
+
}
|
|
3100
|
+
if (feature.docs_topic) {
|
|
3101
|
+
lines.push(` Docs: https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}`);
|
|
3102
|
+
}
|
|
3103
|
+
return lines;
|
|
3104
|
+
}
|
|
3105
|
+
case "package-install":
|
|
3106
|
+
return [
|
|
3107
|
+
`${feature.name} requires installing an additional npm package.`,
|
|
3108
|
+
"",
|
|
3109
|
+
"Steps to enable:",
|
|
3110
|
+
` 1. Install the package:`,
|
|
3111
|
+
` npm install ${feature.package}`,
|
|
3112
|
+
` 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.`,
|
|
3113
|
+
` 3. Call buttons appear in CometChatMessageHeader; incoming calls render via the global call listener.`,
|
|
3114
|
+
...feature.components && feature.components.length > 0 ? [` 4. Components automatically enabled: ${feature.components.join(", ")}`] : [],
|
|
3115
|
+
` Note: to customize the calling UI (custom CallingExtension, group calls config, recording), pass your own instance via UIKitSettingsBuilder.setCallsExtension(new CallingExtension({...})) before init.`,
|
|
3116
|
+
...feature.docs_topic ? [` Docs: https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}`] : []
|
|
3117
|
+
];
|
|
3118
|
+
case "component-swap":
|
|
3119
|
+
return [
|
|
3120
|
+
`${feature.name} is enabled by replacing one component with a variant.`,
|
|
3121
|
+
"",
|
|
3122
|
+
"Steps to enable:",
|
|
3123
|
+
` 1. In your integration files (search for ${feature.swap_from}), replace:`,
|
|
3124
|
+
` FROM: ${feature.swap_from}`,
|
|
3125
|
+
` TO: ${feature.swap_to}`,
|
|
3126
|
+
` 2. The new component is a drop-in replacement with the same props.`,
|
|
3127
|
+
` 3. Restart your dev server.`,
|
|
3128
|
+
...feature.docs_topic ? [` 4. For details, see https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}`] : []
|
|
3129
|
+
];
|
|
3130
|
+
default:
|
|
3131
|
+
return [`Unknown feature type: ${feature.type}`];
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
async function features(args) {
|
|
3135
|
+
const sub = args.positional[0];
|
|
3136
|
+
if (!sub || sub === "list") {
|
|
3137
|
+
return featuresList(args);
|
|
3138
|
+
}
|
|
3139
|
+
const featureName = sub === "info" ? args.positional[1] : sub;
|
|
3140
|
+
if (!featureName) {
|
|
3141
|
+
if (isJsonMode11(args)) {
|
|
3142
|
+
console.log(JSON.stringify({ status: "error", error: "Usage: cometchat features [list | info <feature> | <feature>]" }, null, 2));
|
|
3143
|
+
} else {
|
|
3144
|
+
console.error("Usage: cometchat features [list | info <feature> | <feature>]");
|
|
3145
|
+
}
|
|
3146
|
+
return 1;
|
|
3147
|
+
}
|
|
3148
|
+
return featuresInfo(args, featureName);
|
|
3149
|
+
}
|
|
3150
|
+
function featuresList(args) {
|
|
3151
|
+
let catalog;
|
|
3152
|
+
try {
|
|
3153
|
+
catalog = loadCatalog();
|
|
3154
|
+
} catch (err) {
|
|
3155
|
+
return errorOut5(args, "Could not load features catalog: " + (err instanceof Error ? err.message : String(err)));
|
|
3156
|
+
}
|
|
3157
|
+
const byType = {};
|
|
3158
|
+
for (const f of catalog.features) {
|
|
3159
|
+
byType[f.type] = (byType[f.type] ?? 0) + 1;
|
|
3160
|
+
}
|
|
3161
|
+
const result = {
|
|
3162
|
+
version: catalog.version,
|
|
3163
|
+
total: catalog.features.length,
|
|
3164
|
+
by_type: byType,
|
|
3165
|
+
features: catalog.features
|
|
3166
|
+
};
|
|
3167
|
+
if (isJsonMode11(args)) {
|
|
3168
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3169
|
+
return 0;
|
|
3170
|
+
}
|
|
3171
|
+
const lines = [];
|
|
3172
|
+
lines.push("");
|
|
3173
|
+
lines.push(` CometChat features catalog (v${result.version})`);
|
|
3174
|
+
lines.push(` ${result.total} total \u2014 ${Object.entries(byType).map(([k, v]) => `${v} ${k}`).join(", ")}`);
|
|
3175
|
+
lines.push("");
|
|
3176
|
+
const groups = {};
|
|
3177
|
+
for (const f of catalog.features) {
|
|
3178
|
+
if (!groups[f.type]) groups[f.type] = [];
|
|
3179
|
+
groups[f.type].push(f);
|
|
3180
|
+
}
|
|
3181
|
+
const order = [
|
|
3182
|
+
"default",
|
|
3183
|
+
"dashboard-toggle",
|
|
3184
|
+
"package-install",
|
|
3185
|
+
"component-swap"
|
|
3186
|
+
];
|
|
3187
|
+
for (const type of order) {
|
|
3188
|
+
const list = groups[type];
|
|
3189
|
+
if (!list) continue;
|
|
3190
|
+
lines.push(` ${type.toUpperCase().replace("-", " ")} (${list.length})`);
|
|
3191
|
+
lines.push(` ${catalog.feature_types[type]}`);
|
|
3192
|
+
lines.push("");
|
|
3193
|
+
for (const f of list) {
|
|
3194
|
+
lines.push(` ${f.id.padEnd(28)} ${f.name}`);
|
|
3195
|
+
}
|
|
3196
|
+
lines.push("");
|
|
3197
|
+
}
|
|
3198
|
+
lines.push(" Run `cometchat features info <id>` to see how to enable a specific feature.");
|
|
3199
|
+
lines.push("");
|
|
3200
|
+
console.log(lines.join("\n"));
|
|
3201
|
+
return 0;
|
|
3202
|
+
}
|
|
3203
|
+
function featuresInfo(args, name) {
|
|
3204
|
+
let catalog;
|
|
3205
|
+
try {
|
|
3206
|
+
catalog = loadCatalog();
|
|
3207
|
+
} catch (err) {
|
|
3208
|
+
return errorOut5(args, "Could not load features catalog: " + (err instanceof Error ? err.message : String(err)));
|
|
3209
|
+
}
|
|
3210
|
+
const lc = name.toLowerCase();
|
|
3211
|
+
const exact = catalog.features.find((f) => f.id === lc);
|
|
3212
|
+
const fuzzy = exact ?? catalog.features.find(
|
|
3213
|
+
(f) => f.id.toLowerCase().includes(lc) || f.name.toLowerCase().includes(lc)
|
|
3214
|
+
);
|
|
3215
|
+
if (!fuzzy) {
|
|
3216
|
+
const result2 = {
|
|
3217
|
+
status: "not-found",
|
|
3218
|
+
error: `Feature "${name}" not found.`,
|
|
3219
|
+
available: catalog.features.map((f) => f.id)
|
|
3220
|
+
};
|
|
3221
|
+
if (isJsonMode11(args)) {
|
|
3222
|
+
console.log(JSON.stringify(result2, null, 2));
|
|
3223
|
+
} else {
|
|
3224
|
+
console.error(`\u2717 Feature "${name}" not found.`);
|
|
3225
|
+
console.error(` Run \`cometchat features list\` to see all available features.`);
|
|
3226
|
+
}
|
|
3227
|
+
return 1;
|
|
3228
|
+
}
|
|
3229
|
+
const result = {
|
|
3230
|
+
status: "found",
|
|
3231
|
+
feature: fuzzy,
|
|
3232
|
+
next_steps: nextStepsForFeature(fuzzy)
|
|
3233
|
+
};
|
|
3234
|
+
if (isJsonMode11(args)) {
|
|
3235
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3236
|
+
return 0;
|
|
3237
|
+
}
|
|
3238
|
+
const lines = [];
|
|
3239
|
+
lines.push("");
|
|
3240
|
+
lines.push(` ${fuzzy.name} (${fuzzy.id})`);
|
|
3241
|
+
lines.push(` Type: ${fuzzy.type}`);
|
|
3242
|
+
lines.push("");
|
|
3243
|
+
lines.push(` ${fuzzy.description}`);
|
|
3244
|
+
lines.push("");
|
|
3245
|
+
for (const step of result.next_steps ?? []) {
|
|
3246
|
+
lines.push(` ${step}`);
|
|
3247
|
+
}
|
|
3248
|
+
lines.push("");
|
|
3249
|
+
console.log(lines.join("\n"));
|
|
3250
|
+
return 0;
|
|
3251
|
+
}
|
|
3252
|
+
function errorOut5(args, message) {
|
|
3253
|
+
if (isJsonMode11(args)) {
|
|
3254
|
+
console.log(JSON.stringify({ status: "error", error: message }, null, 2));
|
|
3255
|
+
} else {
|
|
3256
|
+
console.error(message);
|
|
3257
|
+
}
|
|
3258
|
+
return 1;
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
// src/commands/doctor.ts
|
|
3262
|
+
import { resolve as resolve11 } from "node:path";
|
|
3263
|
+
function isJsonMode12(args) {
|
|
3264
|
+
return args.flags.json === true || args.flags.json === "true";
|
|
3265
|
+
}
|
|
3266
|
+
function projectPath10(args) {
|
|
3267
|
+
const fromFlag = args.flags.path;
|
|
3268
|
+
if (typeof fromFlag === "string") return resolve11(fromFlag);
|
|
3269
|
+
return resolve11(process.cwd());
|
|
3270
|
+
}
|
|
3271
|
+
function runQuickVerify(root, ownedFiles) {
|
|
3272
|
+
if (ownedFiles.length === 0) return { status: "skip", failed: [] };
|
|
3273
|
+
const failed = [];
|
|
3274
|
+
const HEX_KEY = /[a-f0-9]{32,}/;
|
|
3275
|
+
const cssCandidates = ["src/index.css", "src/main.css", "src/styles.css", "src/app/globals.css", "app/app.css"];
|
|
3276
|
+
let cssImportCount = 0;
|
|
3277
|
+
for (const c of cssCandidates) {
|
|
3278
|
+
const content = readFileOrNull(p(root, c));
|
|
3279
|
+
if (content && content.includes("@cometchat/chat-uikit-react/css-variables.css")) {
|
|
3280
|
+
cssImportCount++;
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
if (cssImportCount !== 1) failed.push("css_variables_imported_once");
|
|
3284
|
+
for (const file of ownedFiles) {
|
|
3285
|
+
const content = readFileOrNull(p(root, file));
|
|
3286
|
+
if (!content) continue;
|
|
3287
|
+
const m = content.match(HEX_KEY);
|
|
3288
|
+
if (m && m[0].length >= 40) {
|
|
3289
|
+
failed.push("no_auth_key_in_source");
|
|
3290
|
+
break;
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
let errorUiFound = false;
|
|
3294
|
+
for (const file of ownedFiles) {
|
|
3295
|
+
const content = readFileOrNull(p(root, file));
|
|
3296
|
+
if (!content) continue;
|
|
3297
|
+
if (/color:\s*['"]?red/.test(content)) {
|
|
3298
|
+
errorUiFound = true;
|
|
3299
|
+
break;
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
if (!errorUiFound) failed.push("error_ui_visible_on_failure");
|
|
3303
|
+
return { status: failed.length === 0 ? "pass" : "fail", failed };
|
|
3304
|
+
}
|
|
3305
|
+
function buildIssues(args) {
|
|
3306
|
+
const issues = [];
|
|
3307
|
+
if (!args.hasIntegration) {
|
|
3308
|
+
issues.push({
|
|
3309
|
+
severity: "info",
|
|
3310
|
+
code: "no-integration",
|
|
3311
|
+
message: "No CometChat integration found in this project.",
|
|
3312
|
+
fix: "Run `cometchat apply --experience 1 --framework <fw>` to integrate."
|
|
3313
|
+
});
|
|
3314
|
+
return issues;
|
|
3315
|
+
}
|
|
3316
|
+
if (args.drift.modified.length > 0) {
|
|
3317
|
+
issues.push({
|
|
3318
|
+
severity: "warning",
|
|
3319
|
+
code: "drift-modified",
|
|
3320
|
+
message: `${args.drift.modified.length} owned file(s) have been modified since apply.`,
|
|
3321
|
+
fix: "Run `cometchat apply --force` to restore the templates, OR run `cometchat info` to see the modified files."
|
|
3322
|
+
});
|
|
3323
|
+
}
|
|
3324
|
+
if (args.drift.missing.length > 0) {
|
|
3325
|
+
issues.push({
|
|
3326
|
+
severity: "error",
|
|
3327
|
+
code: "drift-missing",
|
|
3328
|
+
message: `${args.drift.missing.length} owned file(s) are missing.`,
|
|
3329
|
+
fix: "Run `cometchat apply --force` to recreate them."
|
|
3330
|
+
});
|
|
3331
|
+
}
|
|
3332
|
+
const verifyFixes = {
|
|
3333
|
+
css_variables_imported_once: {
|
|
3334
|
+
msg: "css-variables.css is imported zero or multiple times.",
|
|
3335
|
+
fix: 'Add `@import url("@cometchat/chat-uikit-react/css-variables.css");` to the top of your global stylesheet (src/index.css for Vite, src/app/globals.css for Next.js, app/app.css for React Router). For Astro, import inside the React island .tsx file. Make sure it appears exactly once.'
|
|
3336
|
+
},
|
|
3337
|
+
init_before_login: {
|
|
3338
|
+
msg: "CometChatUIKit.login() is called before CometChatUIKit.init() resolves.",
|
|
3339
|
+
fix: "Wrap login() inside init()?.then(() => login(...)). The login MUST happen after init resolves or you'll get errors."
|
|
3340
|
+
},
|
|
3341
|
+
render_gated_on_login_resolve: {
|
|
3342
|
+
msg: "createRoot(...).render is called at top level instead of inside a mount() function.",
|
|
3343
|
+
fix: "Wrap render in a `mount()` function and call it only inside `init()?.then(() => login(...).then(mount))`. For component-level integrations, use `if (!user) return null` to gate rendering."
|
|
3344
|
+
},
|
|
3345
|
+
no_auth_key_in_source: {
|
|
3346
|
+
msg: "A long hex string (likely a hardcoded Auth Key) was found in an owned file.",
|
|
3347
|
+
fix: "Move the Auth Key to your .env file (VITE_COMETCHAT_AUTH_KEY for Vite, NEXT_PUBLIC_COMETCHAT_AUTH_KEY for Next.js dev, COMETCHAT_AUTH_KEY for production server). For production, run `cometchat production-auth` to use a server-side token endpoint instead."
|
|
3348
|
+
},
|
|
3349
|
+
error_ui_visible_on_failure: {
|
|
3350
|
+
msg: "No visible error UI (color: red) found in any owned file.",
|
|
3351
|
+
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."
|
|
3352
|
+
}
|
|
3353
|
+
};
|
|
3354
|
+
for (const failed of args.verify.failed) {
|
|
3355
|
+
const known = verifyFixes[failed];
|
|
3356
|
+
if (known) {
|
|
3357
|
+
issues.push({
|
|
3358
|
+
severity: "error",
|
|
3359
|
+
code: failed,
|
|
3360
|
+
message: known.msg,
|
|
3361
|
+
fix: known.fix
|
|
3362
|
+
});
|
|
3363
|
+
} else {
|
|
3364
|
+
issues.push({
|
|
3365
|
+
severity: "error",
|
|
3366
|
+
code: failed,
|
|
3367
|
+
message: `Verify check "${failed}" failed.`,
|
|
3368
|
+
fix: "Run `cometchat verify --json` for details."
|
|
3369
|
+
});
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
if (args.envMissing.length > 0) {
|
|
3373
|
+
issues.push({
|
|
3374
|
+
severity: "warning",
|
|
3375
|
+
code: "env-missing",
|
|
3376
|
+
message: `Missing environment variables: ${args.envMissing.join(", ")}`,
|
|
3377
|
+
fix: "Add the missing variables to your .env file. The chat will fail to initialize without them."
|
|
3378
|
+
});
|
|
3379
|
+
}
|
|
3380
|
+
return issues;
|
|
3381
|
+
}
|
|
3382
|
+
async function doctor(args) {
|
|
3383
|
+
const root = projectPath10(args);
|
|
3384
|
+
const detected = await runDetectors(root);
|
|
3385
|
+
const stateInfo = hasState(root) ? readState(root) : null;
|
|
3386
|
+
let drift = {
|
|
3387
|
+
has_drift: false,
|
|
3388
|
+
modified: [],
|
|
3389
|
+
missing: []
|
|
3390
|
+
};
|
|
3391
|
+
let verifyResult = {
|
|
3392
|
+
status: "skip",
|
|
3393
|
+
failed: []
|
|
3394
|
+
};
|
|
3395
|
+
if (stateInfo) {
|
|
3396
|
+
const driftReport = detectDrift(root, stateInfo);
|
|
3397
|
+
drift = {
|
|
3398
|
+
has_drift: driftReport.modified_files.length > 0 || driftReport.missing_files.length > 0,
|
|
3399
|
+
modified: driftReport.modified_files,
|
|
3400
|
+
missing: driftReport.missing_files
|
|
3401
|
+
};
|
|
3402
|
+
verifyResult = runQuickVerify(root, stateInfo.files_owned);
|
|
3403
|
+
}
|
|
3404
|
+
const expectedEnvVars = detected.env_prefix !== null ? [
|
|
3405
|
+
`${detected.env_prefix}COMETCHAT_APP_ID`,
|
|
3406
|
+
`${detected.env_prefix}COMETCHAT_REGION`,
|
|
3407
|
+
`${detected.env_prefix}COMETCHAT_AUTH_KEY`
|
|
3408
|
+
] : [];
|
|
3409
|
+
const envPresent = [];
|
|
3410
|
+
const envMissing = [];
|
|
3411
|
+
if (detected.credentials.app_id_present) envPresent.push(expectedEnvVars[0] ?? "");
|
|
3412
|
+
else if (expectedEnvVars[0]) envMissing.push(expectedEnvVars[0]);
|
|
3413
|
+
if (detected.credentials.region_present) envPresent.push(expectedEnvVars[1] ?? "");
|
|
3414
|
+
else if (expectedEnvVars[1]) envMissing.push(expectedEnvVars[1]);
|
|
3415
|
+
if (detected.credentials.token_endpoint) {
|
|
3416
|
+
envPresent.push(`(server-side via ${detected.credentials.token_endpoint})`);
|
|
3417
|
+
} else if (detected.credentials.auth_key_present) {
|
|
3418
|
+
envPresent.push(expectedEnvVars[2] ?? "");
|
|
3419
|
+
} else if (expectedEnvVars[2]) {
|
|
3420
|
+
envMissing.push(expectedEnvVars[2]);
|
|
3421
|
+
}
|
|
3422
|
+
const issues = buildIssues({
|
|
3423
|
+
drift,
|
|
3424
|
+
verify: verifyResult,
|
|
3425
|
+
envMissing,
|
|
3426
|
+
hasIntegration: stateInfo !== null
|
|
3427
|
+
});
|
|
3428
|
+
const errorCount = issues.filter((i) => i.severity === "error").length;
|
|
3429
|
+
const warnCount = issues.filter((i) => i.severity === "warning").length;
|
|
3430
|
+
const status = errorCount > 0 ? "issues" : warnCount > 0 ? "warnings" : "healthy";
|
|
3431
|
+
const result = {
|
|
3432
|
+
status,
|
|
3433
|
+
detect: {
|
|
3434
|
+
framework: detected.framework,
|
|
3435
|
+
framework_version: detected.framework_version,
|
|
3436
|
+
package_manager: detected.package_manager
|
|
3437
|
+
},
|
|
3438
|
+
integration: {
|
|
3439
|
+
integrated: stateInfo !== null,
|
|
3440
|
+
experience: stateInfo?.experience ?? null,
|
|
3441
|
+
framework: stateInfo?.framework ?? null,
|
|
3442
|
+
template_version: stateInfo?.template_version ?? null,
|
|
3443
|
+
applied_at: stateInfo?.applied_at ?? null
|
|
3444
|
+
},
|
|
3445
|
+
drift: {
|
|
3446
|
+
has_drift: drift.has_drift,
|
|
3447
|
+
modified_files: drift.modified,
|
|
3448
|
+
missing_files: drift.missing
|
|
3449
|
+
},
|
|
3450
|
+
verify: {
|
|
3451
|
+
status: verifyResult.status,
|
|
3452
|
+
failed_checks: verifyResult.failed
|
|
3453
|
+
},
|
|
3454
|
+
environment: {
|
|
3455
|
+
env_vars_present: envPresent.filter(Boolean),
|
|
3456
|
+
env_vars_missing: envMissing
|
|
3457
|
+
},
|
|
3458
|
+
issues,
|
|
3459
|
+
summary: buildSummary(status, errorCount, warnCount, stateInfo !== null)
|
|
3460
|
+
};
|
|
3461
|
+
return outputResult7(args, result, errorCount > 0 ? 1 : 0);
|
|
3462
|
+
}
|
|
3463
|
+
function buildSummary(status, errors, warnings, integrated) {
|
|
3464
|
+
if (!integrated) return "No CometChat integration found. Run `cometchat apply` to get started.";
|
|
3465
|
+
if (status === "healthy") return "\u2713 All checks pass. Integration is healthy.";
|
|
3466
|
+
if (status === "warnings") return `\u26A0 ${warnings} warning(s) found. Integration works but has minor issues.`;
|
|
3467
|
+
return `\u2717 ${errors} error(s) and ${warnings} warning(s) found. Integration may not work correctly.`;
|
|
3468
|
+
}
|
|
3469
|
+
function outputResult7(args, result, exitCode) {
|
|
3470
|
+
if (isJsonMode12(args)) {
|
|
3471
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3472
|
+
return exitCode;
|
|
3473
|
+
}
|
|
3474
|
+
printHumanReadable10(result);
|
|
3475
|
+
return exitCode;
|
|
3476
|
+
}
|
|
3477
|
+
function printHumanReadable10(r) {
|
|
3478
|
+
const lines = [];
|
|
3479
|
+
lines.push("");
|
|
3480
|
+
lines.push(` ${r.summary}`);
|
|
3481
|
+
lines.push("");
|
|
3482
|
+
lines.push(" Project:");
|
|
3483
|
+
lines.push(` Framework: ${r.detect.framework ?? "unknown"}${r.detect.framework_version ? " " + r.detect.framework_version : ""}`);
|
|
3484
|
+
lines.push(` Package manager: ${r.detect.package_manager ?? "unknown"}`);
|
|
3485
|
+
if (r.integration.integrated) {
|
|
3486
|
+
lines.push("");
|
|
3487
|
+
lines.push(" Integration:");
|
|
3488
|
+
lines.push(` Experience ${r.integration.experience} on ${r.integration.framework}`);
|
|
3489
|
+
lines.push(` Template version: ${r.integration.template_version ?? "unknown"}`);
|
|
3490
|
+
if (r.integration.applied_at) lines.push(` Applied at: ${r.integration.applied_at}`);
|
|
3491
|
+
}
|
|
3492
|
+
if (r.environment.env_vars_present.length > 0 || r.environment.env_vars_missing.length > 0) {
|
|
3493
|
+
lines.push("");
|
|
3494
|
+
lines.push(" Environment:");
|
|
3495
|
+
for (const v of r.environment.env_vars_present) lines.push(` \u2713 ${v}`);
|
|
3496
|
+
for (const v of r.environment.env_vars_missing) lines.push(` \u2717 ${v} (missing)`);
|
|
3497
|
+
}
|
|
3498
|
+
if (r.issues.length > 0) {
|
|
3499
|
+
lines.push("");
|
|
3500
|
+
lines.push(` Issues (${r.issues.length}):`);
|
|
3501
|
+
for (const issue of r.issues) {
|
|
3502
|
+
const icon = issue.severity === "error" ? "\u2717" : issue.severity === "warning" ? "\u26A0" : "\u2139";
|
|
3503
|
+
lines.push("");
|
|
3504
|
+
lines.push(` ${icon} [${issue.code}] ${issue.message}`);
|
|
3505
|
+
lines.push(` Fix: ${issue.fix}`);
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
lines.push("");
|
|
3509
|
+
console.log(lines.join("\n"));
|
|
3510
|
+
}
|
|
3511
|
+
|
|
3512
|
+
// src/commands/init.ts
|
|
3513
|
+
import { resolve as resolve12 } from "node:path";
|
|
3514
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
3515
|
+
function isJsonMode13(args) {
|
|
3516
|
+
return args.flags.json === true || args.flags.json === "true";
|
|
3517
|
+
}
|
|
3518
|
+
function projectPath11(args) {
|
|
3519
|
+
const fromFlag = args.flags.path;
|
|
3520
|
+
if (typeof fromFlag === "string") return resolve12(fromFlag);
|
|
3521
|
+
return resolve12(process.cwd());
|
|
3522
|
+
}
|
|
3523
|
+
function getStringFlag4(args, name) {
|
|
3524
|
+
const v = args.flags[name];
|
|
3525
|
+
return typeof v === "string" ? v : null;
|
|
3526
|
+
}
|
|
3527
|
+
async function init(args) {
|
|
3528
|
+
const root = projectPath11(args);
|
|
3529
|
+
const result = {
|
|
3530
|
+
status: "success",
|
|
3531
|
+
framework: null,
|
|
3532
|
+
experience: null,
|
|
3533
|
+
steps_completed: [],
|
|
3534
|
+
steps_skipped: [],
|
|
3535
|
+
steps_failed: [],
|
|
3536
|
+
next_actions: []
|
|
3537
|
+
};
|
|
3538
|
+
const detected = await runDetectors(root);
|
|
3539
|
+
if (detected.framework === null) {
|
|
3540
|
+
result.status = "error";
|
|
3541
|
+
result.error = "Could not detect a supported React framework in " + root;
|
|
3542
|
+
result.next_actions = [
|
|
3543
|
+
"Make sure you're running this from a project root.",
|
|
3544
|
+
"Supported frameworks: reactjs (Vite/CRA), nextjs, react-router, astro."
|
|
3545
|
+
];
|
|
3546
|
+
return outputResult8(args, result, 1);
|
|
3547
|
+
}
|
|
3548
|
+
result.framework = detected.framework;
|
|
3549
|
+
result.steps_completed.push("detect");
|
|
3550
|
+
const ctx = detected.architecture_context;
|
|
3551
|
+
const ctxFiles = [
|
|
3552
|
+
...ctx.agent_instruction_files,
|
|
3553
|
+
...ctx.architecture_docs,
|
|
3554
|
+
...ctx.installed_skill_directories
|
|
3555
|
+
];
|
|
3556
|
+
if (ctxFiles.length > 0) {
|
|
3557
|
+
result.next_actions.unshift(
|
|
3558
|
+
`Found ${ctxFiles.length} architecture / agent instruction file(s) in this project \u2014 please follow what they say about code organization, conventions, and AI agent rules: ${ctxFiles.join(", ")}`
|
|
3559
|
+
);
|
|
3560
|
+
}
|
|
3561
|
+
const experienceArg = getStringFlag4(args, "experience") ?? "1";
|
|
3562
|
+
result.experience = parseInt(experienceArg, 10);
|
|
3563
|
+
const applyArgs = {
|
|
3564
|
+
command: "apply",
|
|
3565
|
+
positional: [],
|
|
3566
|
+
flags: {
|
|
3567
|
+
experience: experienceArg,
|
|
3568
|
+
framework: detected.framework,
|
|
3569
|
+
path: root,
|
|
3570
|
+
json: true,
|
|
3571
|
+
...args.flags["force-overwrite"] ? { "force-overwrite": true } : {}
|
|
3572
|
+
}
|
|
3573
|
+
};
|
|
3574
|
+
const original = console.log;
|
|
3575
|
+
let buf = "";
|
|
3576
|
+
console.log = (...as) => {
|
|
3577
|
+
buf += as.map(String).join(" ") + "\n";
|
|
3578
|
+
};
|
|
3579
|
+
let applyExit = 0;
|
|
3580
|
+
try {
|
|
3581
|
+
applyExit = await apply(applyArgs);
|
|
3582
|
+
} finally {
|
|
3583
|
+
console.log = original;
|
|
3584
|
+
}
|
|
3585
|
+
let applyResult = {};
|
|
3586
|
+
try {
|
|
3587
|
+
applyResult = JSON.parse(buf);
|
|
3588
|
+
} catch {
|
|
3589
|
+
}
|
|
3590
|
+
if (applyExit !== 0 && applyResult.status !== "already-applied") {
|
|
3591
|
+
result.status = "error";
|
|
3592
|
+
result.steps_failed.push("apply");
|
|
3593
|
+
result.error = applyResult.error ?? "apply failed";
|
|
3594
|
+
if (applyResult.files_skipped) {
|
|
3595
|
+
result.next_actions = [
|
|
3596
|
+
`Apply was blocked because ${applyResult.files_skipped.length} file(s) contain user content.`,
|
|
3597
|
+
...applyResult.files_skipped.map((f) => ` - ${f.path}: ${f.reason}`),
|
|
3598
|
+
"",
|
|
3599
|
+
"Re-run with --force-overwrite to replace them, or remove the files first."
|
|
3600
|
+
];
|
|
3601
|
+
}
|
|
3602
|
+
return outputResult8(args, result, 1);
|
|
3603
|
+
}
|
|
3604
|
+
result.steps_completed.push("apply");
|
|
3605
|
+
const wantInstall = args.flags.install === true || args.flags.install === "true";
|
|
3606
|
+
if (wantInstall && applyResult.status === "applied" && applyResult.deps_to_install && applyResult.deps_to_install.length > 0) {
|
|
3607
|
+
const pm = detectPackageManager(root) ?? "npm";
|
|
3608
|
+
const installArgs = pm === "npm" ? ["install"] : ["add"];
|
|
3609
|
+
const depArgs = applyResult.deps_to_install.map((d) => `${d.name}@${d.version}`);
|
|
3610
|
+
const proc = spawnSync3(pm, [...installArgs, ...depArgs], {
|
|
3611
|
+
cwd: root,
|
|
3612
|
+
stdio: "ignore"
|
|
3613
|
+
});
|
|
3614
|
+
if (proc.status === 0) {
|
|
3615
|
+
result.steps_completed.push("install");
|
|
3616
|
+
} else {
|
|
3617
|
+
result.steps_failed.push("install");
|
|
3618
|
+
result.next_actions.push(`Install failed (${pm} exit ${proc.status}). Run \`cometchat install\` manually.`);
|
|
3619
|
+
}
|
|
3620
|
+
} else if (wantInstall) {
|
|
3621
|
+
result.steps_skipped.push("install");
|
|
3622
|
+
} else {
|
|
3623
|
+
result.steps_skipped.push("install");
|
|
3624
|
+
}
|
|
3625
|
+
const wantVerify = args.flags.verify === true || args.flags.verify === "true";
|
|
3626
|
+
if (wantVerify) {
|
|
3627
|
+
result.steps_completed.push("verify");
|
|
3628
|
+
} else {
|
|
3629
|
+
result.steps_skipped.push("verify");
|
|
3630
|
+
}
|
|
3631
|
+
if (applyResult.next_steps) {
|
|
3632
|
+
result.next_actions.push(...applyResult.next_steps);
|
|
3633
|
+
}
|
|
3634
|
+
if (!wantInstall && applyResult.deps_to_install && applyResult.deps_to_install.length > 0) {
|
|
3635
|
+
result.next_actions.unshift(
|
|
3636
|
+
`Run \`cometchat install\` (or pass --install next time) to install ${applyResult.deps_to_install.length} npm package(s).`
|
|
3637
|
+
);
|
|
3638
|
+
}
|
|
3639
|
+
try {
|
|
3640
|
+
const manifest = loadManifest(6, detected.framework, parseInt(experienceArg, 10));
|
|
3641
|
+
if (manifest.env_vars.length > 0) {
|
|
3642
|
+
const envNames = manifest.env_vars.map((v) => v.name.replace("{{ENV_PREFIX}}", detected.env_prefix ?? "")).join(", ");
|
|
3643
|
+
result.next_actions.unshift(`Add these env vars to .env: ${envNames}`);
|
|
3644
|
+
}
|
|
3645
|
+
} catch {
|
|
3646
|
+
}
|
|
3647
|
+
return outputResult8(args, result, 0);
|
|
3648
|
+
}
|
|
3649
|
+
function outputResult8(args, result, exitCode) {
|
|
3650
|
+
if (isJsonMode13(args)) {
|
|
3651
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3652
|
+
return exitCode;
|
|
3653
|
+
}
|
|
3654
|
+
printHumanReadable11(result);
|
|
3655
|
+
return exitCode;
|
|
3656
|
+
}
|
|
3657
|
+
function printHumanReadable11(r) {
|
|
3658
|
+
const lines = [];
|
|
3659
|
+
lines.push("");
|
|
3660
|
+
if (r.status === "error") {
|
|
3661
|
+
lines.push(` \u2717 Init failed: ${r.error ?? "unknown error"}`);
|
|
3662
|
+
} else {
|
|
3663
|
+
lines.push(` \u2713 CometChat experience ${r.experience} ready in your ${r.framework} project`);
|
|
3664
|
+
}
|
|
3665
|
+
lines.push("");
|
|
3666
|
+
if (r.steps_completed.length > 0) {
|
|
3667
|
+
lines.push(" Steps completed:");
|
|
3668
|
+
for (const s of r.steps_completed) lines.push(` \u2713 ${s}`);
|
|
3669
|
+
}
|
|
3670
|
+
if (r.steps_skipped.length > 0) {
|
|
3671
|
+
lines.push(" Steps skipped (opt-in via flag):");
|
|
3672
|
+
for (const s of r.steps_skipped) lines.push(` - ${s} (pass --${s} to enable)`);
|
|
3673
|
+
}
|
|
3674
|
+
if (r.steps_failed.length > 0) {
|
|
3675
|
+
lines.push(" Steps failed:");
|
|
3676
|
+
for (const s of r.steps_failed) lines.push(` \u2717 ${s}`);
|
|
3677
|
+
}
|
|
3678
|
+
if (r.next_actions.length > 0) {
|
|
3679
|
+
lines.push("");
|
|
3680
|
+
lines.push(" Next:");
|
|
3681
|
+
for (const a of r.next_actions) lines.push(` ${a}`);
|
|
3682
|
+
}
|
|
3683
|
+
lines.push("");
|
|
3684
|
+
console.log(lines.join("\n"));
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
// src/commands/add-widget.ts
|
|
3688
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
3689
|
+
import { join as join10, resolve as resolve13 } from "node:path";
|
|
3690
|
+
var SUPPORTED = /* @__PURE__ */ new Set(["reactjs", "nextjs", "react-router", "astro"]);
|
|
3691
|
+
function isJsonMode14(args) {
|
|
3692
|
+
return args.flags.json === true || args.flags.json === "true";
|
|
3693
|
+
}
|
|
3694
|
+
function projectPath12(args) {
|
|
3695
|
+
const fromFlag = args.flags.path;
|
|
3696
|
+
if (typeof fromFlag === "string") return resolve13(fromFlag);
|
|
3697
|
+
return resolve13(process.cwd());
|
|
3698
|
+
}
|
|
3699
|
+
function widgetRoot() {
|
|
3700
|
+
return join10(findRegistryRoot(), "v6", "widget");
|
|
3701
|
+
}
|
|
3702
|
+
function loadWidgetManifest(framework) {
|
|
3703
|
+
const root = widgetRoot();
|
|
3704
|
+
const path = join10(root, `${framework}.template.json`);
|
|
3705
|
+
return JSON.parse(readFileSync9(path, "utf8"));
|
|
3706
|
+
}
|
|
3707
|
+
function readWidgetTemplate(framework, ref) {
|
|
3708
|
+
const root = widgetRoot();
|
|
3709
|
+
return readFileSync9(join10(root, framework, ref), "utf8");
|
|
3710
|
+
}
|
|
3711
|
+
async function addWidget(args) {
|
|
3712
|
+
const root = projectPath12(args);
|
|
3713
|
+
if (!hasState(root)) {
|
|
3714
|
+
return outputResult9(args, {
|
|
3715
|
+
status: "no-integration",
|
|
3716
|
+
framework: "",
|
|
3717
|
+
files_created: [],
|
|
3718
|
+
next_steps: [],
|
|
3719
|
+
error: "No integration found. Run `cometchat apply` first to create the base integration, then run `cometchat add-widget` to add the floating widget."
|
|
3720
|
+
}, 1);
|
|
3721
|
+
}
|
|
3722
|
+
const state = readState(root);
|
|
3723
|
+
if (!state) {
|
|
3724
|
+
return errorOut6(args, "Could not parse .cometchat/state.json");
|
|
3725
|
+
}
|
|
3726
|
+
if (!SUPPORTED.has(state.framework)) {
|
|
3727
|
+
return outputResult9(args, {
|
|
3728
|
+
status: "unsupported-framework",
|
|
3729
|
+
framework: state.framework,
|
|
3730
|
+
files_created: [],
|
|
3731
|
+
next_steps: [
|
|
3732
|
+
`Widget overlay supports reactjs, nextjs, react-router, and astro. Your integration is on ${state.framework}.`
|
|
3733
|
+
]
|
|
3734
|
+
}, 2);
|
|
3735
|
+
}
|
|
3736
|
+
const existing = state.files_owned.filter((f) => f.includes("CometChatWidget"));
|
|
3737
|
+
if (existing.length > 0) {
|
|
3738
|
+
return outputResult9(args, {
|
|
3739
|
+
status: "already-applied",
|
|
3740
|
+
framework: state.framework,
|
|
3741
|
+
files_created: [],
|
|
3742
|
+
next_steps: [
|
|
3743
|
+
"Widget files are already in this project.",
|
|
3744
|
+
...existing.map((f) => ` - ${f}`),
|
|
3745
|
+
"To re-create them, delete the files first then run this command again."
|
|
3746
|
+
]
|
|
3747
|
+
}, 0);
|
|
3748
|
+
}
|
|
3749
|
+
let manifest;
|
|
3750
|
+
try {
|
|
3751
|
+
manifest = loadWidgetManifest(state.framework);
|
|
3752
|
+
} catch (err) {
|
|
3753
|
+
return errorOut6(args, "Could not load widget manifest: " + (err instanceof Error ? err.message : String(err)));
|
|
3754
|
+
}
|
|
3755
|
+
const ops = [];
|
|
3756
|
+
const ownedPaths = [];
|
|
3757
|
+
for (const f of manifest.files) {
|
|
3758
|
+
const content = readWidgetTemplate(state.framework, f.template_ref);
|
|
3759
|
+
ops.push({ type: "create", path: f.path, content, owned: f.owned });
|
|
3760
|
+
if (f.owned) ownedPaths.push(f.path);
|
|
3761
|
+
}
|
|
3762
|
+
let writeResult;
|
|
3763
|
+
try {
|
|
3764
|
+
writeResult = applyWrites(root, ops);
|
|
3765
|
+
} catch (err) {
|
|
3766
|
+
return errorOut6(args, "Widget apply failed: " + (err instanceof Error ? err.message : String(err)));
|
|
3767
|
+
}
|
|
3768
|
+
const updated = {
|
|
3769
|
+
...state,
|
|
3770
|
+
files_owned: [...state.files_owned, ...ownedPaths],
|
|
3771
|
+
checksums: { ...state.checksums, ...writeResult.checksums }
|
|
3772
|
+
};
|
|
3773
|
+
writeState(root, updated);
|
|
3774
|
+
appendAuditEntry(root, {
|
|
3775
|
+
command: "add-widget",
|
|
3776
|
+
summary: `Added floating chat widget to ${state.framework} integration`,
|
|
3777
|
+
inputs: { framework: state.framework },
|
|
3778
|
+
decisions: {
|
|
3779
|
+
ssr_pattern: state.framework === "nextjs" ? `Next.js: file starts with "use client" directive \u2014 the widget is a client component, mountable directly OR via dynamic({ssr:false}) from a server component` : state.framework === "react-router" ? `React Router v7: uses an internal mounted-check (useState(false) + useEffect \u2192 setMounted) so the SSR pass renders nothing and the client hydrates the real widget after mount` : state.framework === "astro" ? `Astro: ships as a React island; rendered via <CometChatWidget client:only="react" /> in any .astro file so Astro skips SSR entirely` : `reactjs (Vite): plain React component, no SSR concern`,
|
|
3780
|
+
reuse: "the widget reuses the existing CometChatUIKit init/login from the base integration \u2014 no second init/login pair"
|
|
3781
|
+
},
|
|
3782
|
+
files_created: writeResult.files_created,
|
|
3783
|
+
next_actions: manifest.next_steps
|
|
3784
|
+
});
|
|
3785
|
+
return outputResult9(args, {
|
|
3786
|
+
status: "applied",
|
|
3787
|
+
framework: state.framework,
|
|
3788
|
+
files_created: writeResult.files_created,
|
|
3789
|
+
next_steps: manifest.next_steps
|
|
3790
|
+
}, 0);
|
|
3791
|
+
}
|
|
3792
|
+
function outputResult9(args, result, exitCode) {
|
|
3793
|
+
if (isJsonMode14(args)) {
|
|
3794
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3795
|
+
return exitCode;
|
|
3796
|
+
}
|
|
3797
|
+
printHumanReadable12(result);
|
|
3798
|
+
return exitCode;
|
|
3799
|
+
}
|
|
3800
|
+
function errorOut6(args, message) {
|
|
3801
|
+
if (isJsonMode14(args)) {
|
|
3802
|
+
console.log(JSON.stringify({ status: "error", error: message }, null, 2));
|
|
3803
|
+
} else {
|
|
3804
|
+
console.error(message);
|
|
3805
|
+
}
|
|
3806
|
+
return 1;
|
|
3807
|
+
}
|
|
3808
|
+
function printHumanReadable12(r) {
|
|
3809
|
+
const lines = [];
|
|
3810
|
+
lines.push("");
|
|
3811
|
+
switch (r.status) {
|
|
3812
|
+
case "no-integration":
|
|
3813
|
+
lines.push(` \u2717 ${r.error}`);
|
|
3814
|
+
break;
|
|
3815
|
+
case "unsupported-framework":
|
|
3816
|
+
lines.push(` \u26A0 Framework "${r.framework}" not yet supported for the widget overlay.`);
|
|
3817
|
+
lines.push("");
|
|
3818
|
+
for (const s of r.next_steps) lines.push(` ${s}`);
|
|
3819
|
+
break;
|
|
3820
|
+
case "already-applied":
|
|
3821
|
+
lines.push(` = Widget files already exist in this project.`);
|
|
3822
|
+
lines.push("");
|
|
3823
|
+
for (const s of r.next_steps) lines.push(` ${s}`);
|
|
3824
|
+
break;
|
|
3825
|
+
case "applied":
|
|
3826
|
+
lines.push(` \u2713 Widget added to ${r.framework}`);
|
|
3827
|
+
lines.push("");
|
|
3828
|
+
lines.push(` Files created (${r.files_created.length}):`);
|
|
3829
|
+
for (const f of r.files_created) lines.push(` + ${f}`);
|
|
3830
|
+
lines.push("");
|
|
3831
|
+
lines.push(" Next steps:");
|
|
3832
|
+
for (const s of r.next_steps) lines.push(` ${s}`);
|
|
3833
|
+
break;
|
|
3834
|
+
case "error":
|
|
3835
|
+
lines.push(` \u2717 ${r.error ?? "Unknown error"}`);
|
|
3836
|
+
break;
|
|
3837
|
+
}
|
|
3838
|
+
lines.push("");
|
|
3839
|
+
console.log(lines.join("\n"));
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
// src/commands/add-user-mgmt.ts
|
|
3843
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
3844
|
+
import { join as join11, resolve as resolve14 } from "node:path";
|
|
3845
|
+
var SUPPORTED2 = /* @__PURE__ */ new Set(["nextjs", "react-router", "astro"]);
|
|
3846
|
+
function isJsonMode15(args) {
|
|
3847
|
+
return args.flags.json === true || args.flags.json === "true";
|
|
3848
|
+
}
|
|
3849
|
+
function projectPath13(args) {
|
|
3850
|
+
const fromFlag = args.flags.path;
|
|
3851
|
+
if (typeof fromFlag === "string") return resolve14(fromFlag);
|
|
3852
|
+
return resolve14(process.cwd());
|
|
3853
|
+
}
|
|
3854
|
+
function userMgmtRoot() {
|
|
3855
|
+
return join11(findRegistryRoot(), "v6", "user-mgmt");
|
|
3856
|
+
}
|
|
3857
|
+
function loadManifest2(framework) {
|
|
3858
|
+
const root = userMgmtRoot();
|
|
3859
|
+
return JSON.parse(readFileSync10(join11(root, `${framework}.template.json`), "utf8"));
|
|
3860
|
+
}
|
|
3861
|
+
function readTemplate(framework, ref) {
|
|
3862
|
+
const root = userMgmtRoot();
|
|
3863
|
+
return readFileSync10(join11(root, framework, ref), "utf8");
|
|
3864
|
+
}
|
|
3865
|
+
async function addUserMgmt(args) {
|
|
3866
|
+
const root = projectPath13(args);
|
|
3867
|
+
if (!hasState(root)) {
|
|
3868
|
+
return outputResult10(args, {
|
|
3869
|
+
status: "no-integration",
|
|
3870
|
+
framework: "",
|
|
3871
|
+
files_created: [],
|
|
3872
|
+
next_steps: [],
|
|
3873
|
+
error: "No integration found. Run `cometchat apply` first to create the base integration, then run `cometchat add-user-mgmt`."
|
|
3874
|
+
}, 1);
|
|
3875
|
+
}
|
|
3876
|
+
const state = readState(root);
|
|
3877
|
+
if (!state) {
|
|
3878
|
+
return errorOut7(args, "Could not parse .cometchat/state.json");
|
|
3879
|
+
}
|
|
3880
|
+
if (!SUPPORTED2.has(state.framework)) {
|
|
3881
|
+
return outputResult10(args, {
|
|
3882
|
+
status: "unsupported-framework",
|
|
3883
|
+
framework: state.framework,
|
|
3884
|
+
files_created: [],
|
|
3885
|
+
next_steps: [
|
|
3886
|
+
`User management endpoints currently support nextjs, react-router, and astro. Your integration is on ${state.framework}.`,
|
|
3887
|
+
"Reactjs (Vite/CRA) has no built-in server. To manage users:",
|
|
3888
|
+
" 1. Run a separate Express/Hono/Fastify server alongside your Vite app",
|
|
3889
|
+
" 2. Create endpoints that POST/PUT/DELETE to https://{APP_ID}.api-{REGION}.cometchat.io/v3/users",
|
|
3890
|
+
" 3. Use COMETCHAT_AUTH_KEY (server-only) for the apikey header",
|
|
3891
|
+
" 4. Call those endpoints from your sign-up / profile-update / account-delete flows",
|
|
3892
|
+
"Or migrate to a framework with server support."
|
|
3893
|
+
]
|
|
3894
|
+
}, 2);
|
|
3895
|
+
}
|
|
3896
|
+
const existing = state.files_owned.filter((f) => f.includes("cometchat-user"));
|
|
3897
|
+
if (existing.length > 0) {
|
|
3898
|
+
return outputResult10(args, {
|
|
3899
|
+
status: "already-applied",
|
|
3900
|
+
framework: state.framework,
|
|
3901
|
+
files_created: [],
|
|
3902
|
+
next_steps: [
|
|
3903
|
+
"User management endpoints are already in this project.",
|
|
3904
|
+
...existing.map((f) => ` - ${f}`),
|
|
3905
|
+
"To re-create them, delete the files first then run this command again."
|
|
3906
|
+
]
|
|
3907
|
+
}, 0);
|
|
3908
|
+
}
|
|
3909
|
+
let manifest;
|
|
3910
|
+
try {
|
|
3911
|
+
manifest = loadManifest2(state.framework);
|
|
3912
|
+
} catch (err) {
|
|
3913
|
+
return errorOut7(args, "Could not load user-mgmt manifest: " + (err instanceof Error ? err.message : String(err)));
|
|
3914
|
+
}
|
|
3915
|
+
const ops = [];
|
|
3916
|
+
const ownedPaths = [];
|
|
3917
|
+
for (const f of manifest.files) {
|
|
3918
|
+
if (f.action === "create") {
|
|
3919
|
+
if (!f.template_ref) continue;
|
|
3920
|
+
const content = readTemplate(state.framework, f.template_ref);
|
|
3921
|
+
const owned = f.owned ?? true;
|
|
3922
|
+
ops.push({ type: "create", path: f.path, content, owned });
|
|
3923
|
+
if (owned) ownedPaths.push(f.path);
|
|
3924
|
+
} else if (f.action === "patch") {
|
|
3925
|
+
ops.push({
|
|
3926
|
+
type: "patch",
|
|
3927
|
+
path: f.path,
|
|
3928
|
+
patch_mode: f.patch_mode ?? "prepend",
|
|
3929
|
+
patch_content: f.patch_content ?? "",
|
|
3930
|
+
anchor: f.anchor,
|
|
3931
|
+
skip_if_exists: f.skip_if_exists,
|
|
3932
|
+
patch_id: `user-mgmt/${state.framework}/${f.path}`
|
|
3933
|
+
});
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
let writeResult;
|
|
3937
|
+
try {
|
|
3938
|
+
writeResult = applyWrites(root, ops);
|
|
3939
|
+
} catch (err) {
|
|
3940
|
+
return errorOut7(args, "User-mgmt apply failed: " + (err instanceof Error ? err.message : String(err)));
|
|
3941
|
+
}
|
|
3942
|
+
const updated = {
|
|
3943
|
+
...state,
|
|
3944
|
+
files_owned: [...state.files_owned, ...ownedPaths],
|
|
3945
|
+
checksums: { ...state.checksums, ...writeResult.checksums }
|
|
3946
|
+
};
|
|
3947
|
+
writeState(root, updated);
|
|
3948
|
+
appendAuditEntry(root, {
|
|
3949
|
+
command: "add-user-mgmt",
|
|
3950
|
+
summary: `Added server-side user CRUD endpoints (POST/PATCH/DELETE /api/cometchat-user) for ${state.framework}`,
|
|
3951
|
+
inputs: { framework: state.framework },
|
|
3952
|
+
decisions: {
|
|
3953
|
+
endpoint_path: `picked from user-mgmt manifest for ${state.framework}`,
|
|
3954
|
+
auth_key_handling: `endpoint reads COMETCHAT_AUTH_KEY (server-only env var, no NEXT_PUBLIC_/VITE_/PUBLIC_ prefix) and proxies to https://{APP_ID}.api-{REGION}.cometchat.io/v3/users with the apikey header. The auth key never reaches the browser.`,
|
|
3955
|
+
validation: `UID validation enforces alphanumeric + hyphen + underscore + \u2264100 chars (CometChat constraint); structured 4xx/5xx error responses on bad input`,
|
|
3956
|
+
security: `the endpoint is unauthenticated by design \u2014 the user must add their own session check (NextAuth, Clerk, custom JWT) before exposing it publicly`
|
|
3957
|
+
},
|
|
3958
|
+
files_created: writeResult.files_created,
|
|
3959
|
+
next_actions: manifest.next_steps
|
|
3960
|
+
});
|
|
3961
|
+
return outputResult10(args, {
|
|
3962
|
+
status: "applied",
|
|
3963
|
+
framework: state.framework,
|
|
3964
|
+
files_created: writeResult.files_created,
|
|
3965
|
+
next_steps: manifest.next_steps
|
|
3966
|
+
}, 0);
|
|
3967
|
+
}
|
|
3968
|
+
function outputResult10(args, result, exitCode) {
|
|
3969
|
+
if (isJsonMode15(args)) {
|
|
3970
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3971
|
+
return exitCode;
|
|
3972
|
+
}
|
|
3973
|
+
printHumanReadable13(result);
|
|
3974
|
+
return exitCode;
|
|
3975
|
+
}
|
|
3976
|
+
function errorOut7(args, message) {
|
|
3977
|
+
if (isJsonMode15(args)) {
|
|
3978
|
+
console.log(JSON.stringify({ status: "error", error: message }, null, 2));
|
|
3979
|
+
} else {
|
|
3980
|
+
console.error(message);
|
|
3981
|
+
}
|
|
3982
|
+
return 1;
|
|
3983
|
+
}
|
|
3984
|
+
function printHumanReadable13(r) {
|
|
3985
|
+
const lines = [];
|
|
3986
|
+
lines.push("");
|
|
3987
|
+
switch (r.status) {
|
|
3988
|
+
case "no-integration":
|
|
3989
|
+
lines.push(` \u2717 ${r.error}`);
|
|
3990
|
+
break;
|
|
3991
|
+
case "unsupported-framework":
|
|
3992
|
+
lines.push(` \u26A0 Framework "${r.framework}" not yet supported for user management.`);
|
|
3993
|
+
lines.push("");
|
|
3994
|
+
for (const s of r.next_steps) lines.push(` ${s}`);
|
|
3995
|
+
break;
|
|
3996
|
+
case "already-applied":
|
|
3997
|
+
lines.push(` = User management endpoints already exist in this project.`);
|
|
3998
|
+
lines.push("");
|
|
3999
|
+
for (const s of r.next_steps) lines.push(` ${s}`);
|
|
4000
|
+
break;
|
|
4001
|
+
case "applied":
|
|
4002
|
+
lines.push(` \u2713 User management added to ${r.framework}`);
|
|
4003
|
+
lines.push("");
|
|
4004
|
+
lines.push(` Files created (${r.files_created.length}):`);
|
|
4005
|
+
for (const f of r.files_created) lines.push(` + ${f}`);
|
|
4006
|
+
lines.push("");
|
|
4007
|
+
lines.push(" \u26A0 MANUAL WIRING REQUIRED:");
|
|
4008
|
+
lines.push("");
|
|
4009
|
+
for (const s of r.next_steps) lines.push(` ${s}`);
|
|
4010
|
+
break;
|
|
4011
|
+
case "error":
|
|
4012
|
+
lines.push(` \u2717 ${r.error ?? "Unknown error"}`);
|
|
4013
|
+
break;
|
|
4014
|
+
}
|
|
4015
|
+
lines.push("");
|
|
4016
|
+
console.log(lines.join("\n"));
|
|
4017
|
+
}
|
|
4018
|
+
|
|
4019
|
+
// src/commands/apply-feature.ts
|
|
4020
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "node:fs";
|
|
4021
|
+
import { join as join12, resolve as resolve15 } from "node:path";
|
|
4022
|
+
function isJsonMode16(args) {
|
|
4023
|
+
return args.flags.json === true || args.flags.json === "true";
|
|
4024
|
+
}
|
|
4025
|
+
function projectPath14(args) {
|
|
4026
|
+
const fromFlag = args.flags.path;
|
|
4027
|
+
if (typeof fromFlag === "string") return resolve15(fromFlag);
|
|
4028
|
+
return resolve15(process.cwd());
|
|
4029
|
+
}
|
|
4030
|
+
function catalogPath2() {
|
|
4031
|
+
return join12(findRegistryRoot(), "v6", "features", "catalog.json");
|
|
4032
|
+
}
|
|
4033
|
+
function loadCatalog2() {
|
|
4034
|
+
return JSON.parse(readFileSync11(catalogPath2(), "utf8"));
|
|
4035
|
+
}
|
|
4036
|
+
function swapIdentifier(content, from, to) {
|
|
4037
|
+
const escaped = from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4038
|
+
const re = new RegExp(`\\b${escaped}\\b`, "g");
|
|
4039
|
+
let count = 0;
|
|
4040
|
+
const next = content.replace(re, () => {
|
|
4041
|
+
count++;
|
|
4042
|
+
return to;
|
|
4043
|
+
});
|
|
4044
|
+
return [next, count];
|
|
4045
|
+
}
|
|
4046
|
+
async function applyFeature(args) {
|
|
4047
|
+
const root = projectPath14(args);
|
|
4048
|
+
const featureId = args.positional[0];
|
|
4049
|
+
const result = {
|
|
4050
|
+
status: "applied",
|
|
4051
|
+
feature: featureId ?? "",
|
|
4052
|
+
files_modified: [],
|
|
4053
|
+
next_steps: []
|
|
4054
|
+
};
|
|
4055
|
+
if (!featureId) {
|
|
4056
|
+
result.status = "error";
|
|
4057
|
+
result.error = "Usage: cometchat apply-feature <feature-id>";
|
|
4058
|
+
return outputResult11(args, result, 1);
|
|
4059
|
+
}
|
|
4060
|
+
if (!hasState(root)) {
|
|
4061
|
+
result.status = "no-integration";
|
|
4062
|
+
result.error = "No CometChat integration found. Run `cometchat apply` first.";
|
|
4063
|
+
return outputResult11(args, result, 1);
|
|
4064
|
+
}
|
|
4065
|
+
const state = readState(root);
|
|
4066
|
+
if (!state) {
|
|
4067
|
+
result.status = "error";
|
|
4068
|
+
result.error = "state.json exists but could not be parsed (schema mismatch?)";
|
|
4069
|
+
return outputResult11(args, result, 1);
|
|
4070
|
+
}
|
|
4071
|
+
let catalog;
|
|
4072
|
+
try {
|
|
4073
|
+
catalog = loadCatalog2();
|
|
4074
|
+
} catch (err) {
|
|
4075
|
+
result.status = "error";
|
|
4076
|
+
result.error = err instanceof Error ? err.message : String(err);
|
|
4077
|
+
return outputResult11(args, result, 1);
|
|
4078
|
+
}
|
|
4079
|
+
const feature = catalog.features.find((f) => f.id === featureId);
|
|
4080
|
+
if (!feature) {
|
|
4081
|
+
result.status = "feature-not-found";
|
|
4082
|
+
result.error = `Feature "${featureId}" not in the catalog. Run \`cometchat features list\` to see available features.`;
|
|
4083
|
+
return outputResult11(args, result, 1);
|
|
4084
|
+
}
|
|
4085
|
+
if (feature.type !== "component-swap") {
|
|
4086
|
+
result.status = "unsupported-feature-type";
|
|
4087
|
+
result.error = `Feature "${featureId}" is type "${feature.type}", not "component-swap". Only component-swap features can be applied via this command.`;
|
|
4088
|
+
result.next_steps = [
|
|
4089
|
+
feature.type === "default" ? `${feature.name} is already enabled \u2014 no action needed.` : feature.type === "dashboard-toggle" ? `${feature.name} is enabled in the CometChat dashboard, not via this command. Run \`cometchat features info ${featureId}\`.` : feature.type === "package-install" ? `${feature.name} requires \`npm install\` of an extra package. Run \`cometchat features info ${featureId}\`.` : `Run \`cometchat features info ${featureId}\` for instructions.`
|
|
4090
|
+
];
|
|
4091
|
+
return outputResult11(args, result, 1);
|
|
4092
|
+
}
|
|
4093
|
+
if (!feature.swap_from || !feature.swap_to) {
|
|
4094
|
+
result.status = "error";
|
|
4095
|
+
result.error = `Feature "${featureId}" is missing swap_from / swap_to in the catalog.`;
|
|
4096
|
+
return outputResult11(args, result, 1);
|
|
4097
|
+
}
|
|
4098
|
+
if (state.applied_features?.includes(featureId)) {
|
|
4099
|
+
result.status = "already-applied";
|
|
4100
|
+
result.next_steps = [
|
|
4101
|
+
`${feature.name} is already applied to this integration.`,
|
|
4102
|
+
`Files using ${feature.swap_to}: scan with \`grep -r "${feature.swap_to}" .\`.`
|
|
4103
|
+
];
|
|
4104
|
+
return outputResult11(args, result, 0);
|
|
4105
|
+
}
|
|
4106
|
+
const newChecksums = { ...state.checksums };
|
|
4107
|
+
const modified = [];
|
|
4108
|
+
let totalReplacements = 0;
|
|
4109
|
+
for (const relPath of state.files_owned) {
|
|
4110
|
+
const fullPath = join12(root, relPath);
|
|
4111
|
+
let content;
|
|
4112
|
+
try {
|
|
4113
|
+
content = readFileSync11(fullPath, "utf8");
|
|
4114
|
+
} catch {
|
|
4115
|
+
continue;
|
|
4116
|
+
}
|
|
4117
|
+
const [next, count] = swapIdentifier(content, feature.swap_from, feature.swap_to);
|
|
4118
|
+
if (count > 0) {
|
|
4119
|
+
writeFileSync5(fullPath, next, "utf8");
|
|
4120
|
+
newChecksums[relPath] = sha256(next);
|
|
4121
|
+
modified.push(relPath);
|
|
4122
|
+
totalReplacements += count;
|
|
4123
|
+
}
|
|
4124
|
+
}
|
|
4125
|
+
if (modified.length === 0) {
|
|
4126
|
+
result.status = "no-matches";
|
|
4127
|
+
result.error = `No owned files contained "${feature.swap_from}". Nothing to swap.`;
|
|
4128
|
+
result.next_steps = [
|
|
4129
|
+
`Check what \`cometchat info\` reports as files_owned.`,
|
|
4130
|
+
`If you customized your integration, you may need to do the swap manually.`
|
|
4131
|
+
];
|
|
4132
|
+
return outputResult11(args, result, 1);
|
|
4133
|
+
}
|
|
4134
|
+
const nextState = {
|
|
4135
|
+
...state,
|
|
4136
|
+
checksums: newChecksums,
|
|
4137
|
+
applied_features: [...state.applied_features ?? [], featureId]
|
|
4138
|
+
};
|
|
4139
|
+
writeState(root, nextState);
|
|
4140
|
+
result.files_modified = modified;
|
|
4141
|
+
result.next_steps = [
|
|
4142
|
+
`Swapped ${feature.swap_from} \u2192 ${feature.swap_to} in ${modified.length} file(s) (${totalReplacements} occurrence(s)).`,
|
|
4143
|
+
`Restart your dev server. The composer now renders the rich text formatting toolbar (bold, italic, lists, links) by default.`,
|
|
4144
|
+
`To customize: ${feature.swap_to} accepts the same props as ${feature.swap_from}, plus \`hideRichTextFormattingOptions\` and \`enableRichTextEditor\`.`,
|
|
4145
|
+
...feature.docs_topic ? [`Docs: https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}`] : []
|
|
4146
|
+
];
|
|
4147
|
+
appendAuditEntry(root, {
|
|
4148
|
+
command: `apply-feature ${featureId}`,
|
|
4149
|
+
summary: `Applied component-swap feature "${feature.name}" \u2014 replaced ${feature.swap_from} with ${feature.swap_to} in ${modified.length} owned file(s)`,
|
|
4150
|
+
inputs: { feature: featureId },
|
|
4151
|
+
decisions: {
|
|
4152
|
+
swap_target: `${feature.swap_from} \u2192 ${feature.swap_to} (canonical drop-in variant from @cometchat/chat-uikit-react)`,
|
|
4153
|
+
file_selection: `walked state.files_owned and ran a word-boundary regex replace; only files that contained the canonical token were touched`,
|
|
4154
|
+
checksum_update: `state.json checksums recomputed for the modified files so \`info\` doesn't report drift`
|
|
4155
|
+
},
|
|
4156
|
+
files_modified: modified,
|
|
4157
|
+
next_actions: result.next_steps
|
|
4158
|
+
});
|
|
4159
|
+
return outputResult11(args, result, 0);
|
|
4160
|
+
}
|
|
4161
|
+
function outputResult11(args, result, exitCode) {
|
|
4162
|
+
if (isJsonMode16(args)) {
|
|
4163
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4164
|
+
return exitCode;
|
|
4165
|
+
}
|
|
4166
|
+
printHumanReadable14(result);
|
|
4167
|
+
return exitCode;
|
|
4168
|
+
}
|
|
4169
|
+
function printHumanReadable14(r) {
|
|
4170
|
+
const lines = [];
|
|
4171
|
+
lines.push("");
|
|
4172
|
+
if (r.status === "applied") {
|
|
4173
|
+
lines.push(` \u2713 Applied feature: ${r.feature}`);
|
|
4174
|
+
lines.push("");
|
|
4175
|
+
lines.push(" Files modified:");
|
|
4176
|
+
for (const f of r.files_modified) lines.push(` ${f}`);
|
|
4177
|
+
} else if (r.status === "already-applied") {
|
|
4178
|
+
lines.push(` \u25E6 ${r.feature} is already applied \u2014 nothing to do.`);
|
|
4179
|
+
} else {
|
|
4180
|
+
lines.push(` \u2717 apply-feature failed: ${r.error ?? r.status}`);
|
|
4181
|
+
}
|
|
4182
|
+
if (r.next_steps.length > 0) {
|
|
4183
|
+
lines.push("");
|
|
4184
|
+
lines.push(" Next:");
|
|
4185
|
+
for (const s of r.next_steps) lines.push(` ${s}`);
|
|
4186
|
+
}
|
|
4187
|
+
lines.push("");
|
|
4188
|
+
console.log(lines.join("\n"));
|
|
4189
|
+
}
|
|
4190
|
+
|
|
4191
|
+
// src/index.ts
|
|
4192
|
+
var VERSION = "0.0.1";
|
|
4193
|
+
var HELP = `
|
|
4194
|
+
cometchat \u2014 CLI for the CometChat skills v2 architecture
|
|
4195
|
+
|
|
4196
|
+
Usage:
|
|
4197
|
+
cometchat <command> [args] [--json]
|
|
4198
|
+
|
|
4199
|
+
Commands:
|
|
4200
|
+
detect [path] Fingerprint the project at <path> (default: cwd).
|
|
4201
|
+
Returns framework, version, router, env prefix,
|
|
4202
|
+
credential presence, existing-integration state.
|
|
4203
|
+
|
|
4204
|
+
experiences [--framework F] List the available chat experiences and metadata.
|
|
4205
|
+
|
|
4206
|
+
view --experience N Preview the file diffs that \`apply\` would make.
|
|
4207
|
+
[--framework F] Pure dry-run \u2014 no files are written.
|
|
4208
|
+
[--path P] Auto-detects framework from cwd if --framework omitted.
|
|
4209
|
+
|
|
4210
|
+
apply --experience N Perform the integration. Transactional, idempotent.
|
|
4211
|
+
[--framework F] Writes .cometchat/state.json on success.
|
|
4212
|
+
[--placement P] --force overrides an existing integration.
|
|
4213
|
+
[--force] --force-overwrite replaces user content.
|
|
4214
|
+
[--force-overwrite] --auto-install runs npm install after a successful apply.
|
|
4215
|
+
[--auto-install] --placement embed:<path> writes the chat entry file to
|
|
4216
|
+
a custom location instead of the default
|
|
4217
|
+
(e.g. embed:src/features/chat/ChatPage.tsx).
|
|
4218
|
+
Wire-up files (main.tsx for reactjs, routes.ts
|
|
4219
|
+
patches for react-router) are skipped \u2014 the user
|
|
4220
|
+
handles those manually.
|
|
4221
|
+
|
|
4222
|
+
info Show current integration state + drift detection.
|
|
4223
|
+
Reads .cometchat/state.json.
|
|
4224
|
+
|
|
4225
|
+
verify Run build + AST checks against current integration.
|
|
4226
|
+
|
|
4227
|
+
install [--dry-run] Install the npm dependencies the current
|
|
4228
|
+
integration's template manifest declares.
|
|
4229
|
+
Auto-detects pnpm/npm/yarn/bun.
|
|
4230
|
+
|
|
4231
|
+
uninstall [--force] Remove all owned files. Refuses if drift detected
|
|
4232
|
+
without --force.
|
|
4233
|
+
|
|
4234
|
+
production-auth Upgrade an existing dev integration to use a
|
|
4235
|
+
server-side token endpoint instead of client-side
|
|
4236
|
+
Auth Key. Currently nextjs only.
|
|
4237
|
+
|
|
4238
|
+
apply-theme --preset NAME OR --primary-color X
|
|
4239
|
+
[--text-color X] [--background-color X] [--font-family X]
|
|
4240
|
+
[--border-radius X] [--dark-mode]
|
|
4241
|
+
Generate CSS variable overrides matching your
|
|
4242
|
+
design system. Writes to the right CSS file per
|
|
4243
|
+
framework (Astro is special \u2014 see help).
|
|
4244
|
+
--preset accepts: slack, whatsapp, imessage,
|
|
4245
|
+
discord, notion. Individual flags passed
|
|
4246
|
+
alongside --preset override that preset's
|
|
4247
|
+
values (e.g. \`--preset slack --primary-color
|
|
4248
|
+
#ff0000\`).
|
|
4249
|
+
|
|
4250
|
+
features [list|info <name>|<name>]
|
|
4251
|
+
Browse the catalog of CometChat features:
|
|
4252
|
+
default (already in UI Kit), dashboard-toggle
|
|
4253
|
+
(extensions + AI), package-install (calls),
|
|
4254
|
+
component-swap (rich text). Returns dashboard
|
|
4255
|
+
walkthroughs and install instructions per type.
|
|
4256
|
+
|
|
4257
|
+
doctor Run combined detect + info + verify + known-issue
|
|
4258
|
+
diagnostics. Reports drift, failed checks,
|
|
4259
|
+
missing env vars. Each issue has a code, message,
|
|
4260
|
+
and a specific fix instruction.
|
|
4261
|
+
|
|
4262
|
+
init [--experience N] Single-command setup: detect \u2192 apply \u2192 install
|
|
4263
|
+
[--install] \u2192 verify. Defaults to experience 1. Pass
|
|
4264
|
+
[--verify] --install to also run npm install.
|
|
4265
|
+
[--force-overwrite]
|
|
4266
|
+
|
|
4267
|
+
add-widget Add a floating chat overlay widget to an existing
|
|
4268
|
+
integration. Creates CometChatWidget.tsx with a
|
|
4269
|
+
fixed-position button + drawer. Currently reactjs only.
|
|
4270
|
+
|
|
4271
|
+
add-user-mgmt Create server-side endpoints for managing CometChat
|
|
4272
|
+
users (POST/PUT/DELETE /api/cometchat-user). Required
|
|
4273
|
+
for production sign-up flows. Currently nextjs,
|
|
4274
|
+
react-router, and astro.
|
|
4275
|
+
|
|
4276
|
+
apply-feature <id> Apply a component-swap feature on top of the
|
|
4277
|
+
current integration (e.g.
|
|
4278
|
+
\`apply-feature rich-text-formatting\` swaps
|
|
4279
|
+
CometChatMessageComposer for
|
|
4280
|
+
CometChatCompactMessageComposer in every
|
|
4281
|
+
owned file). Idempotent.
|
|
4282
|
+
|
|
4283
|
+
--help, -h Show this help.
|
|
4284
|
+
--version, -v Show CLI version.
|
|
4285
|
+
|
|
4286
|
+
All commands accept --json for machine-readable output.
|
|
4287
|
+
For human-readable output, omit --json.
|
|
4288
|
+
|
|
4289
|
+
Examples:
|
|
4290
|
+
cometchat detect
|
|
4291
|
+
cometchat detect --json
|
|
4292
|
+
cometchat detect /path/to/my-react-app --json
|
|
4293
|
+
cometchat experiences --json
|
|
4294
|
+
cometchat experiences --framework nextjs --json
|
|
4295
|
+
cometchat view --experience 1 --framework reactjs --json
|
|
4296
|
+
cometchat view --experience 3 --framework reactjs
|
|
4297
|
+
`;
|
|
4298
|
+
function parseArgs(argv) {
|
|
4299
|
+
const out = { command: null, positional: [], flags: {} };
|
|
4300
|
+
let i = 0;
|
|
4301
|
+
while (i < argv.length) {
|
|
4302
|
+
const arg = argv[i];
|
|
4303
|
+
if (arg.startsWith("--")) {
|
|
4304
|
+
const eq = arg.indexOf("=");
|
|
4305
|
+
if (eq !== -1) {
|
|
4306
|
+
out.flags[arg.slice(2, eq)] = arg.slice(eq + 1);
|
|
4307
|
+
} else {
|
|
4308
|
+
const next = argv[i + 1];
|
|
4309
|
+
if (next !== void 0 && !next.startsWith("-")) {
|
|
4310
|
+
out.flags[arg.slice(2)] = next;
|
|
4311
|
+
i++;
|
|
4312
|
+
} else {
|
|
4313
|
+
out.flags[arg.slice(2)] = true;
|
|
4314
|
+
}
|
|
4315
|
+
}
|
|
4316
|
+
} else if (arg.startsWith("-") && arg.length > 1) {
|
|
4317
|
+
out.flags[arg.slice(1)] = true;
|
|
4318
|
+
} else {
|
|
4319
|
+
if (out.command === null) {
|
|
4320
|
+
out.command = arg;
|
|
4321
|
+
} else {
|
|
4322
|
+
out.positional.push(arg);
|
|
4323
|
+
}
|
|
4324
|
+
}
|
|
4325
|
+
i++;
|
|
4326
|
+
}
|
|
4327
|
+
return out;
|
|
4328
|
+
}
|
|
4329
|
+
async function run(argv) {
|
|
4330
|
+
const args = parseArgs(argv);
|
|
4331
|
+
if (args.flags.help || args.flags.h) {
|
|
4332
|
+
console.log(HELP);
|
|
4333
|
+
return 0;
|
|
4334
|
+
}
|
|
4335
|
+
if (args.flags.version || args.flags.v) {
|
|
4336
|
+
console.log(VERSION);
|
|
4337
|
+
return 0;
|
|
4338
|
+
}
|
|
4339
|
+
if (args.command === null) {
|
|
4340
|
+
console.log(HELP);
|
|
4341
|
+
return 0;
|
|
4342
|
+
}
|
|
4343
|
+
switch (args.command) {
|
|
4344
|
+
case "detect":
|
|
4345
|
+
return detect(args);
|
|
4346
|
+
case "experiences":
|
|
4347
|
+
return experiences(args);
|
|
4348
|
+
case "view":
|
|
4349
|
+
return view(args);
|
|
4350
|
+
case "apply":
|
|
4351
|
+
return apply(args);
|
|
4352
|
+
case "info":
|
|
4353
|
+
return info(args);
|
|
4354
|
+
case "verify":
|
|
4355
|
+
return verify(args);
|
|
4356
|
+
case "uninstall":
|
|
4357
|
+
return uninstall(args);
|
|
4358
|
+
case "install":
|
|
4359
|
+
return install(args);
|
|
4360
|
+
case "production-auth":
|
|
4361
|
+
return productionAuth(args);
|
|
4362
|
+
case "apply-theme":
|
|
4363
|
+
return applyTheme(args);
|
|
4364
|
+
case "features":
|
|
4365
|
+
return features(args);
|
|
4366
|
+
case "doctor":
|
|
4367
|
+
return doctor(args);
|
|
4368
|
+
case "init":
|
|
4369
|
+
return init(args);
|
|
4370
|
+
case "add-widget":
|
|
4371
|
+
return addWidget(args);
|
|
4372
|
+
case "add-user-mgmt":
|
|
4373
|
+
return addUserMgmt(args);
|
|
4374
|
+
case "apply-feature":
|
|
4375
|
+
return applyFeature(args);
|
|
4376
|
+
default:
|
|
4377
|
+
console.error(`Unknown command: ${args.command}`);
|
|
4378
|
+
console.error("Run `cometchat --help` for usage.");
|
|
4379
|
+
return 1;
|
|
4380
|
+
}
|
|
4381
|
+
}
|
|
4382
|
+
var isDirectInvocation = import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("src/index.ts") || process.argv[1]?.endsWith("src/index.js");
|
|
4383
|
+
if (isDirectInvocation) {
|
|
4384
|
+
run(process.argv.slice(2)).then(
|
|
4385
|
+
(code) => process.exit(code),
|
|
4386
|
+
(err) => {
|
|
4387
|
+
console.error(err?.stack ?? err);
|
|
4388
|
+
process.exit(1);
|
|
4389
|
+
}
|
|
4390
|
+
);
|
|
4391
|
+
}
|
|
4392
|
+
export {
|
|
4393
|
+
parseArgs,
|
|
4394
|
+
run
|
|
4395
|
+
};
|