@schandlergarcia/sf-web-components 2.0.0 → 2.1.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/CHANGELOG.md +10 -0
- package/CLAUDE.md +23 -0
- package/brands/engine/agentApiConfig.ts +36 -0
- package/brands/engine/brand.css +40 -0
- package/brands/engine/engine-command-center-prd.md +575 -0
- package/brands/engine/engine-live-data.js +135 -0
- package/brands/engine/engine-sample-data.js +378 -0
- package/brands/engine/engine_logo.png +0 -0
- package/brands/engine/global.css +234 -0
- package/brands/engine/schema.graphql +292 -0
- package/brands/engine/useEngineLiveData.ts +49 -0
- package/brands/engine/useEvaAgent.ts +288 -0
- package/package.json +6 -2
- package/scripts/apply-brand.mjs +178 -0
- package/scripts/postinstall.mjs +14 -0
- package/scripts/reset-command-center.sh +28 -7
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useEvaAgent — React hook for the Agentforce Agent REST API
|
|
3
|
+
*
|
|
4
|
+
* Handles the full lifecycle:
|
|
5
|
+
* 1. OAuth client-credentials → access_token
|
|
6
|
+
* 2. POST …/sessions → sessionId + message href
|
|
7
|
+
* 3. POST …/messages → agent response text
|
|
8
|
+
* 4. DELETE …/sessions → cleanup on unmount
|
|
9
|
+
*
|
|
10
|
+
* All HTTP calls go through the Vite proxy (see vite.config.ts):
|
|
11
|
+
* /sf-oauth/* → myDomainUrl
|
|
12
|
+
* /sf-agent/* → agentApiBaseUrl
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
16
|
+
import { AGENT_API_CONFIG } from "@/config/agentApi";
|
|
17
|
+
|
|
18
|
+
export interface AgentMessage {
|
|
19
|
+
role: "user" | "agent";
|
|
20
|
+
text: string;
|
|
21
|
+
id: string;
|
|
22
|
+
timestamp: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface SessionState {
|
|
26
|
+
accessToken: string;
|
|
27
|
+
sessionId: string;
|
|
28
|
+
messagesHref: string;
|
|
29
|
+
endHref: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default function useEvaAgent() {
|
|
33
|
+
const [messages, setMessages] = useState<AgentMessage[]>([]);
|
|
34
|
+
const [isConnecting, setIsConnecting] = useState(false);
|
|
35
|
+
const [isReady, setIsReady] = useState(false);
|
|
36
|
+
const [isSending, setIsSending] = useState(false);
|
|
37
|
+
const [error, setError] = useState<string | null>(null);
|
|
38
|
+
|
|
39
|
+
const sessionRef = useRef<SessionState | null>(null);
|
|
40
|
+
const sequenceRef = useRef(1);
|
|
41
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
42
|
+
|
|
43
|
+
/* ── Step 1: OAuth token ──────────────────────────────── */
|
|
44
|
+
async function authenticate(signal: AbortSignal): Promise<string> {
|
|
45
|
+
const { clientId, clientSecret } = AGENT_API_CONFIG;
|
|
46
|
+
|
|
47
|
+
const body = new URLSearchParams({
|
|
48
|
+
grant_type: "client_credentials",
|
|
49
|
+
client_id: clientId,
|
|
50
|
+
client_secret: clientSecret,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const res = await fetch("/sf-oauth/services/oauth2/token", {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
56
|
+
body,
|
|
57
|
+
signal,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
const text = await res.text();
|
|
62
|
+
throw new Error(`OAuth failed (${res.status}): ${text}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const data = await res.json();
|
|
66
|
+
return data.access_token;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* ── Step 2: Create session ───────────────────────────── */
|
|
70
|
+
async function createSession(
|
|
71
|
+
accessToken: string,
|
|
72
|
+
signal: AbortSignal
|
|
73
|
+
): Promise<Omit<SessionState, "accessToken">> {
|
|
74
|
+
const { agentId, bypassUser, demoTraveler, myDomainUrl } =
|
|
75
|
+
AGENT_API_CONFIG;
|
|
76
|
+
|
|
77
|
+
const payload = {
|
|
78
|
+
externalSessionKey: crypto.randomUUID(),
|
|
79
|
+
instanceConfig: { endpoint: myDomainUrl },
|
|
80
|
+
streamingCapabilities: { chunkTypes: ["Text"] },
|
|
81
|
+
bypassUser,
|
|
82
|
+
variables: [
|
|
83
|
+
{
|
|
84
|
+
name: "$Context.EndUserId",
|
|
85
|
+
type: "Text",
|
|
86
|
+
value: demoTraveler.contactId,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "$Context.EndUserEmail",
|
|
90
|
+
type: "Text",
|
|
91
|
+
value: demoTraveler.email,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "$Context.EndUserFirstName",
|
|
95
|
+
type: "Text",
|
|
96
|
+
value: demoTraveler.firstName,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "$Context.EndUserLastName",
|
|
100
|
+
type: "Text",
|
|
101
|
+
value: demoTraveler.lastName,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const res = await fetch(
|
|
107
|
+
`/sf-agent/einstein/ai-agent/v1/agents/${agentId}/sessions`,
|
|
108
|
+
{
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: {
|
|
111
|
+
Authorization: `Bearer ${accessToken}`,
|
|
112
|
+
"Content-Type": "application/json",
|
|
113
|
+
},
|
|
114
|
+
body: JSON.stringify(payload),
|
|
115
|
+
signal,
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
if (!res.ok) {
|
|
120
|
+
const text = await res.text();
|
|
121
|
+
throw new Error(`Session creation failed (${res.status}): ${text}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const data = await res.json();
|
|
125
|
+
return {
|
|
126
|
+
sessionId: data.sessionId,
|
|
127
|
+
messagesHref: data._links?.messages?.href ?? "",
|
|
128
|
+
endHref: data._links?.end?.href ?? "",
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* ── Step 3: Send message ─────────────────────────────── */
|
|
133
|
+
const sendMessage = useCallback(async (text: string) => {
|
|
134
|
+
const session = sessionRef.current;
|
|
135
|
+
if (!session) return;
|
|
136
|
+
|
|
137
|
+
const userMsg: AgentMessage = {
|
|
138
|
+
role: "user",
|
|
139
|
+
text,
|
|
140
|
+
id: `user-${Date.now()}`,
|
|
141
|
+
timestamp: new Date().toISOString(),
|
|
142
|
+
};
|
|
143
|
+
setMessages((prev) => [...prev, userMsg]);
|
|
144
|
+
setIsSending(true);
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const seqId = sequenceRef.current++;
|
|
148
|
+
|
|
149
|
+
const messagesUrl = session.messagesHref.startsWith("http")
|
|
150
|
+
? `/sf-agent${new URL(session.messagesHref).pathname}`
|
|
151
|
+
: `/sf-agent${session.messagesHref}`;
|
|
152
|
+
|
|
153
|
+
const res = await fetch(messagesUrl, {
|
|
154
|
+
method: "POST",
|
|
155
|
+
headers: {
|
|
156
|
+
Authorization: `Bearer ${session.accessToken}`,
|
|
157
|
+
"Content-Type": "application/json",
|
|
158
|
+
Accept: "application/json",
|
|
159
|
+
},
|
|
160
|
+
body: JSON.stringify({
|
|
161
|
+
message: { sequenceId: seqId, type: "Text", text },
|
|
162
|
+
}),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (!res.ok) {
|
|
166
|
+
const errText = await res.text();
|
|
167
|
+
throw new Error(`Agent message failed (${res.status}): ${errText}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const data = await res.json();
|
|
171
|
+
|
|
172
|
+
const agentTexts: string[] = [];
|
|
173
|
+
if (Array.isArray(data.messages)) {
|
|
174
|
+
for (const m of data.messages) {
|
|
175
|
+
if (m.type === "Inform" && m.message) {
|
|
176
|
+
agentTexts.push(m.message);
|
|
177
|
+
} else if (m.type === "Text" && m.message) {
|
|
178
|
+
agentTexts.push(m.message);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const responseText =
|
|
184
|
+
agentTexts.length > 0
|
|
185
|
+
? agentTexts.join("\n\n")
|
|
186
|
+
: data.message ?? "No response from agent.";
|
|
187
|
+
|
|
188
|
+
const agentMsg: AgentMessage = {
|
|
189
|
+
role: "agent",
|
|
190
|
+
text: responseText,
|
|
191
|
+
id: `agent-${Date.now()}`,
|
|
192
|
+
timestamp: new Date().toISOString(),
|
|
193
|
+
};
|
|
194
|
+
setMessages((prev) => [...prev, agentMsg]);
|
|
195
|
+
} catch (err: unknown) {
|
|
196
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
197
|
+
setError(msg);
|
|
198
|
+
const errMsg: AgentMessage = {
|
|
199
|
+
role: "agent",
|
|
200
|
+
text: `Error: ${msg}`,
|
|
201
|
+
id: `error-${Date.now()}`,
|
|
202
|
+
timestamp: new Date().toISOString(),
|
|
203
|
+
};
|
|
204
|
+
setMessages((prev) => [...prev, errMsg]);
|
|
205
|
+
} finally {
|
|
206
|
+
setIsSending(false);
|
|
207
|
+
}
|
|
208
|
+
}, []);
|
|
209
|
+
|
|
210
|
+
/* ── Step 4: End session ──────────────────────────────── */
|
|
211
|
+
const endSession = useCallback(async () => {
|
|
212
|
+
const session = sessionRef.current;
|
|
213
|
+
if (!session) return;
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const endUrl = session.endHref.startsWith("http")
|
|
217
|
+
? `/sf-agent${new URL(session.endHref).pathname}`
|
|
218
|
+
: `/sf-agent${session.endHref}`;
|
|
219
|
+
|
|
220
|
+
await fetch(endUrl, {
|
|
221
|
+
method: "DELETE",
|
|
222
|
+
headers: { Authorization: `Bearer ${session.accessToken}` },
|
|
223
|
+
});
|
|
224
|
+
} catch {
|
|
225
|
+
/* best-effort cleanup */
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
sessionRef.current = null;
|
|
229
|
+
setIsReady(false);
|
|
230
|
+
}, []);
|
|
231
|
+
|
|
232
|
+
/* ── Init: authenticate + create session on mount ─────── */
|
|
233
|
+
const connect = useCallback(async () => {
|
|
234
|
+
if (sessionRef.current) return;
|
|
235
|
+
|
|
236
|
+
abortRef.current = new AbortController();
|
|
237
|
+
const { signal } = abortRef.current;
|
|
238
|
+
|
|
239
|
+
setIsConnecting(true);
|
|
240
|
+
setError(null);
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const accessToken = await authenticate(signal);
|
|
244
|
+
const session = await createSession(accessToken, signal);
|
|
245
|
+
|
|
246
|
+
sessionRef.current = { accessToken, ...session };
|
|
247
|
+
sequenceRef.current = 1;
|
|
248
|
+
setIsReady(true);
|
|
249
|
+
} catch (err: unknown) {
|
|
250
|
+
if ((err as Error).name !== "AbortError") {
|
|
251
|
+
const msg = err instanceof Error ? err.message : "Connection failed";
|
|
252
|
+
setError(msg);
|
|
253
|
+
}
|
|
254
|
+
} finally {
|
|
255
|
+
setIsConnecting(false);
|
|
256
|
+
}
|
|
257
|
+
}, []);
|
|
258
|
+
|
|
259
|
+
/* ── Cleanup on unmount ───────────────────────────────── */
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
return () => {
|
|
262
|
+
abortRef.current?.abort();
|
|
263
|
+
if (sessionRef.current) {
|
|
264
|
+
const session = sessionRef.current;
|
|
265
|
+
const endUrl = session.endHref.startsWith("http")
|
|
266
|
+
? `/sf-agent${new URL(session.endHref).pathname}`
|
|
267
|
+
: `/sf-agent${session.endHref}`;
|
|
268
|
+
|
|
269
|
+
fetch(endUrl, {
|
|
270
|
+
method: "DELETE",
|
|
271
|
+
headers: { Authorization: `Bearer ${session.accessToken}` },
|
|
272
|
+
}).catch(() => {});
|
|
273
|
+
sessionRef.current = null;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}, []);
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
messages,
|
|
280
|
+
isConnecting,
|
|
281
|
+
isReady,
|
|
282
|
+
isSending,
|
|
283
|
+
error,
|
|
284
|
+
connect,
|
|
285
|
+
sendMessage,
|
|
286
|
+
endSession,
|
|
287
|
+
};
|
|
288
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schandlergarcia/sf-web-components",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Reusable Salesforce web components library with Tailwind CSS v4 and shadcn/ui",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"files": [
|
|
28
28
|
"dist",
|
|
29
29
|
"scripts",
|
|
30
|
+
"brands",
|
|
30
31
|
"src/templates",
|
|
31
32
|
"src/components",
|
|
32
33
|
"src/lib",
|
|
@@ -50,7 +51,10 @@
|
|
|
50
51
|
"postinstall": "node scripts/postinstall.mjs",
|
|
51
52
|
"reset:command-center": "bash scripts/reset-command-center.sh",
|
|
52
53
|
"validate:dashboard": "bash scripts/validate-dashboard.sh",
|
|
53
|
-
"graphql:schema": "node scripts/get-graphql-schema.mjs"
|
|
54
|
+
"graphql:schema": "node scripts/get-graphql-schema.mjs",
|
|
55
|
+
"brand:engine": "node node_modules/@schandlergarcia/sf-web-components/scripts/apply-brand.mjs engine",
|
|
56
|
+
"brand:reset": "node node_modules/@schandlergarcia/sf-web-components/scripts/apply-brand.mjs --reset",
|
|
57
|
+
"brand:list": "node node_modules/@schandlergarcia/sf-web-components/scripts/apply-brand.mjs --list"
|
|
54
58
|
},
|
|
55
59
|
"peerDependencies": {
|
|
56
60
|
"@heroicons/react": "^2.0.0",
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* apply-brand.mjs — Apply a brand theme to the project.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node scripts/apply-brand.mjs engine # Apply Engine brand
|
|
8
|
+
* node scripts/apply-brand.mjs --list # List available brands
|
|
9
|
+
* node scripts/apply-brand.mjs --reset # Remove brand, restore neutral theme
|
|
10
|
+
*
|
|
11
|
+
* When run from a consuming project (via npm run brand:engine), the script
|
|
12
|
+
* detects the package location automatically.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from 'fs';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import { fileURLToPath } from 'url';
|
|
18
|
+
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __dirname = path.dirname(__filename);
|
|
21
|
+
|
|
22
|
+
const arg = process.argv[2];
|
|
23
|
+
|
|
24
|
+
if (!arg) {
|
|
25
|
+
console.error('Usage: node scripts/apply-brand.mjs <brand-name|--list|--reset>');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const cwd = process.cwd();
|
|
30
|
+
const PACKAGE_NAME = '@schandlergarcia/sf-web-components';
|
|
31
|
+
|
|
32
|
+
function findPackageRoot() {
|
|
33
|
+
const fromNodeModules = path.join(cwd, 'node_modules', PACKAGE_NAME);
|
|
34
|
+
if (fs.existsSync(fromNodeModules)) return fromNodeModules;
|
|
35
|
+
|
|
36
|
+
const fromScript = path.resolve(__dirname, '..');
|
|
37
|
+
if (fs.existsSync(path.join(fromScript, 'brands'))) return fromScript;
|
|
38
|
+
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const packageRoot = findPackageRoot();
|
|
43
|
+
if (!packageRoot) {
|
|
44
|
+
console.error('Could not find package root. Run from the project directory.');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const brandsDir = path.join(packageRoot, 'brands');
|
|
49
|
+
|
|
50
|
+
if (arg === '--list') {
|
|
51
|
+
if (!fs.existsSync(brandsDir)) {
|
|
52
|
+
console.log('No brands available.');
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
const brands = fs.readdirSync(brandsDir).filter(d =>
|
|
56
|
+
fs.statSync(path.join(brandsDir, d)).isDirectory()
|
|
57
|
+
);
|
|
58
|
+
console.log(`Available brands: ${brands.join(', ') || '(none)'}`);
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (arg === '--reset') {
|
|
63
|
+
const neutralCSS = path.join(packageRoot, 'src/styles/global.css');
|
|
64
|
+
const targetCSS = path.join(cwd, 'src/styles/global.css');
|
|
65
|
+
if (fs.existsSync(neutralCSS)) {
|
|
66
|
+
fs.copyFileSync(neutralCSS, targetCSS);
|
|
67
|
+
console.log(' ✓ Restored neutral theme (global.css)');
|
|
68
|
+
}
|
|
69
|
+
const brandMarker = path.join(cwd, '.brand');
|
|
70
|
+
if (fs.existsSync(brandMarker)) fs.unlinkSync(brandMarker);
|
|
71
|
+
console.log('\n✅ Brand reset to neutral.\n');
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const brandName = arg;
|
|
76
|
+
const brandDir = path.join(brandsDir, brandName);
|
|
77
|
+
|
|
78
|
+
if (!fs.existsSync(brandDir)) {
|
|
79
|
+
console.error(`Brand "${brandName}" not found in ${brandsDir}`);
|
|
80
|
+
const available = fs.existsSync(brandsDir)
|
|
81
|
+
? fs.readdirSync(brandsDir).filter(d => fs.statSync(path.join(brandsDir, d)).isDirectory())
|
|
82
|
+
: [];
|
|
83
|
+
if (available.length) console.error(`Available: ${available.join(', ')}`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log(`\n🎨 Applying "${brandName}" brand...\n`);
|
|
88
|
+
|
|
89
|
+
let installed = 0;
|
|
90
|
+
|
|
91
|
+
// 1. global.css → src/styles/global.css (always overwrite)
|
|
92
|
+
const brandCSS = path.join(brandDir, 'global.css');
|
|
93
|
+
const targetCSS = path.join(cwd, 'src/styles/global.css');
|
|
94
|
+
if (fs.existsSync(brandCSS)) {
|
|
95
|
+
fs.mkdirSync(path.dirname(targetCSS), { recursive: true });
|
|
96
|
+
fs.copyFileSync(brandCSS, targetCSS);
|
|
97
|
+
console.log(' ✓ Brand theme applied (global.css)');
|
|
98
|
+
installed++;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 2. Sample data files → src/data/
|
|
102
|
+
const dataFiles = [
|
|
103
|
+
{ src: 'engine-sample-data.js', dst: 'src/data/engine-sample-data.js' },
|
|
104
|
+
{ src: 'engine-live-data.js', dst: 'src/data/engine-live-data.js' },
|
|
105
|
+
];
|
|
106
|
+
for (const { src, dst } of dataFiles) {
|
|
107
|
+
const srcPath = path.join(brandDir, src);
|
|
108
|
+
const dstPath = path.join(cwd, dst);
|
|
109
|
+
if (fs.existsSync(srcPath)) {
|
|
110
|
+
fs.mkdirSync(path.dirname(dstPath), { recursive: true });
|
|
111
|
+
fs.copyFileSync(srcPath, dstPath);
|
|
112
|
+
console.log(` ✓ Installed ${dst}`);
|
|
113
|
+
installed++;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 3. Hooks → src/hooks/
|
|
118
|
+
const hooks = [
|
|
119
|
+
{ src: 'useEngineLiveData.ts', dst: 'src/hooks/useEngineLiveData.ts' },
|
|
120
|
+
{ src: 'useEvaAgent.ts', dst: 'src/hooks/useEvaAgent.ts' },
|
|
121
|
+
];
|
|
122
|
+
for (const { src, dst } of hooks) {
|
|
123
|
+
const srcPath = path.join(brandDir, src);
|
|
124
|
+
const dstPath = path.join(cwd, dst);
|
|
125
|
+
if (fs.existsSync(srcPath)) {
|
|
126
|
+
fs.mkdirSync(path.dirname(dstPath), { recursive: true });
|
|
127
|
+
fs.copyFileSync(srcPath, dstPath);
|
|
128
|
+
console.log(` ✓ Installed ${dst}`);
|
|
129
|
+
installed++;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 4. Config files → src/config/
|
|
134
|
+
const configs = [
|
|
135
|
+
{ src: 'agentApiConfig.ts', dst: 'src/config/agentApi.ts' },
|
|
136
|
+
];
|
|
137
|
+
for (const { src, dst } of configs) {
|
|
138
|
+
const srcPath = path.join(brandDir, src);
|
|
139
|
+
const dstPath = path.join(cwd, dst);
|
|
140
|
+
if (fs.existsSync(srcPath)) {
|
|
141
|
+
fs.mkdirSync(path.dirname(dstPath), { recursive: true });
|
|
142
|
+
fs.copyFileSync(srcPath, dstPath);
|
|
143
|
+
console.log(` ✓ Installed ${dst}`);
|
|
144
|
+
installed++;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 5. PRD → project root
|
|
149
|
+
const prd = path.join(brandDir, 'engine-command-center-prd.md');
|
|
150
|
+
if (fs.existsSync(prd)) {
|
|
151
|
+
fs.copyFileSync(prd, path.join(cwd, 'engine-command-center-prd.md'));
|
|
152
|
+
console.log(' ✓ Installed engine-command-center-prd.md');
|
|
153
|
+
installed++;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 6. Schema → project root
|
|
157
|
+
const schema = path.join(brandDir, 'schema.graphql');
|
|
158
|
+
if (fs.existsSync(schema)) {
|
|
159
|
+
fs.copyFileSync(schema, path.join(cwd, 'schema.graphql'));
|
|
160
|
+
console.log(' ✓ Installed schema.graphql');
|
|
161
|
+
installed++;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 7. Logo → src/assets/images/
|
|
165
|
+
const logo = path.join(brandDir, 'engine_logo.png');
|
|
166
|
+
if (fs.existsSync(logo)) {
|
|
167
|
+
const logoTarget = path.join(cwd, 'src/assets/images/engine_logo.png');
|
|
168
|
+
fs.mkdirSync(path.dirname(logoTarget), { recursive: true });
|
|
169
|
+
fs.copyFileSync(logo, logoTarget);
|
|
170
|
+
console.log(' ✓ Installed engine_logo.png');
|
|
171
|
+
installed++;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 8. Write .brand marker so reset script knows which brand is active
|
|
175
|
+
fs.writeFileSync(path.join(cwd, '.brand'), brandName + '\n', 'utf-8');
|
|
176
|
+
|
|
177
|
+
console.log(`\n✅ "${brandName}" brand applied (${installed} files installed).`);
|
|
178
|
+
console.log(' Run "npm run brand:reset" to revert to neutral theme.\n');
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -335,6 +335,20 @@ if (fs.existsSync(packageJsonPath)) {
|
|
|
335
335
|
scriptsAdded.push('validate:dashboard');
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
+
// Add brand scripts
|
|
339
|
+
if (!packageJson.scripts['brand:engine']) {
|
|
340
|
+
packageJson.scripts['brand:engine'] = 'node node_modules/@schandlergarcia/sf-web-components/scripts/apply-brand.mjs engine';
|
|
341
|
+
scriptsAdded.push('brand:engine');
|
|
342
|
+
}
|
|
343
|
+
if (!packageJson.scripts['brand:reset']) {
|
|
344
|
+
packageJson.scripts['brand:reset'] = 'node node_modules/@schandlergarcia/sf-web-components/scripts/apply-brand.mjs --reset';
|
|
345
|
+
scriptsAdded.push('brand:reset');
|
|
346
|
+
}
|
|
347
|
+
if (!packageJson.scripts['brand:list']) {
|
|
348
|
+
packageJson.scripts['brand:list'] = 'node node_modules/@schandlergarcia/sf-web-components/scripts/apply-brand.mjs --list';
|
|
349
|
+
scriptsAdded.push('brand:list');
|
|
350
|
+
}
|
|
351
|
+
|
|
338
352
|
if (scriptsAdded.length > 0) {
|
|
339
353
|
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8');
|
|
340
354
|
scriptsAdded.forEach(script => {
|
|
@@ -398,24 +398,45 @@ echo "→ Cleaning caches…"
|
|
|
398
398
|
rm -rf node_modules/.vite 2>/dev/null && echo " ✓ Cleared Vite cache" || true
|
|
399
399
|
echo ""
|
|
400
400
|
|
|
401
|
-
# ── 9. Restore
|
|
401
|
+
# ── 9. Restore global.css (brand-aware) ──────────────────────────────────────
|
|
402
402
|
|
|
403
403
|
mkdir -p src/styles
|
|
404
404
|
|
|
405
405
|
GLOBAL_CSS="src/styles/global.css"
|
|
406
406
|
echo "→ Restoring ${GLOBAL_CSS}..."
|
|
407
407
|
|
|
408
|
-
#
|
|
408
|
+
# Check if a brand is active
|
|
409
|
+
ACTIVE_BRAND=""
|
|
410
|
+
if [ -f ".brand" ]; then
|
|
411
|
+
ACTIVE_BRAND="$(cat .brand | tr -d '[:space:]')"
|
|
412
|
+
fi
|
|
413
|
+
|
|
409
414
|
PACKAGE_CSS=""
|
|
410
|
-
if [ -
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
415
|
+
if [ -n "$ACTIVE_BRAND" ]; then
|
|
416
|
+
# Use the brand's global.css
|
|
417
|
+
if [ -f "node_modules/@schandlergarcia/sf-web-components/brands/$ACTIVE_BRAND/global.css" ]; then
|
|
418
|
+
PACKAGE_CSS="node_modules/@schandlergarcia/sf-web-components/brands/$ACTIVE_BRAND/global.css"
|
|
419
|
+
elif [ -f "$SCRIPT_DIR/../brands/$ACTIVE_BRAND/global.css" ]; then
|
|
420
|
+
PACKAGE_CSS="$SCRIPT_DIR/../brands/$ACTIVE_BRAND/global.css"
|
|
421
|
+
fi
|
|
422
|
+
fi
|
|
423
|
+
|
|
424
|
+
# Fall back to neutral if no brand or brand CSS not found
|
|
425
|
+
if [ -z "$PACKAGE_CSS" ]; then
|
|
426
|
+
if [ -f "node_modules/@schandlergarcia/sf-web-components/src/styles/global.css" ]; then
|
|
427
|
+
PACKAGE_CSS="node_modules/@schandlergarcia/sf-web-components/src/styles/global.css"
|
|
428
|
+
elif [ -f "$SCRIPT_DIR/../src/styles/global.css" ]; then
|
|
429
|
+
PACKAGE_CSS="$SCRIPT_DIR/../src/styles/global.css"
|
|
430
|
+
fi
|
|
414
431
|
fi
|
|
415
432
|
|
|
416
433
|
if [ -n "$PACKAGE_CSS" ] && [ -f "$PACKAGE_CSS" ]; then
|
|
417
434
|
cp "$PACKAGE_CSS" "$GLOBAL_CSS"
|
|
418
|
-
|
|
435
|
+
if [ -n "$ACTIVE_BRAND" ]; then
|
|
436
|
+
echo " ✓ Theme restored (brand: $ACTIVE_BRAND)"
|
|
437
|
+
else
|
|
438
|
+
echo " ✓ Theme restored (neutral)"
|
|
439
|
+
fi
|
|
419
440
|
else
|
|
420
441
|
echo " ⚠ Skipped global.css (package CSS not found)"
|
|
421
442
|
fi
|