@salesforce/templates 66.7.13 → 66.9.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.
Files changed (105) hide show
  1. package/lib/generators/lightningEmbeddingGenerator.d.ts +6 -0
  2. package/lib/generators/lightningEmbeddingGenerator.js +68 -0
  3. package/lib/generators/lightningEmbeddingGenerator.js.map +1 -0
  4. package/lib/i18n/i18n.d.ts +6 -0
  5. package/lib/i18n/i18n.js +6 -0
  6. package/lib/i18n/i18n.js.map +1 -1
  7. package/lib/index.d.ts +1 -0
  8. package/lib/index.js +1 -0
  9. package/lib/index.js.map +1 -1
  10. package/lib/templates/lightningembedding/default/default.css +5 -0
  11. package/lib/templates/lightningembedding/default/default.html +7 -0
  12. package/lib/templates/lightningembedding/default/default.js +5 -0
  13. package/lib/templates/lightningembedding/default/default.js-meta.xml +12 -0
  14. package/lib/templates/project/reactexternalapp/AGENT.md +3 -3
  15. package/lib/templates/project/reactexternalapp/CHANGELOG.md +428 -0
  16. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/package-lock.json +792 -2031
  17. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/package.json +4 -4
  18. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/pages/AccountObjectDetailPage.tsx +7 -9
  19. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/pages/AccountSearch.tsx +7 -15
  20. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/api/objectSearchService.ts +14 -19
  21. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/NumericRangeFilter.tsx +9 -5
  22. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/SearchFilter.tsx +5 -3
  23. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/TextFilter.tsx +5 -3
  24. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/hooks/useAsyncData.ts +11 -4
  25. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/api/userProfileApi.ts +12 -11
  26. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/authenticationConfig.ts +9 -9
  27. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/context/AuthContext.tsx +21 -4
  28. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/forms/auth-form.tsx +15 -1
  29. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/hooks/form.tsx +1 -1
  30. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/layouts/privateRouteLayout.tsx +2 -11
  31. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/ChangePassword.tsx +21 -5
  32. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/ForgotPassword.tsx +20 -5
  33. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/Login.tsx +20 -5
  34. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/Profile.tsx +80 -43
  35. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/Register.tsx +16 -5
  36. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/ResetPassword.tsx +20 -5
  37. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/utils/helpers.ts +15 -52
  38. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/api/graphqlClient.ts +13 -13
  39. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/components/alerts/status-alert.tsx +11 -8
  40. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/components/ui/input.tsx +1 -1
  41. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/hooks/useAsyncData.ts +67 -0
  42. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/pages/AccountObjectDetailPage.tsx +7 -9
  43. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/pages/AccountSearch.tsx +7 -15
  44. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/routes.tsx +19 -25
  45. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/tsconfig.json +4 -6
  46. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/vite-env.d.ts +0 -3
  47. package/lib/templates/project/reactexternalapp/_p_/_m_/classes/UIBundleAuthUtils.cls-meta.xml +1 -1
  48. package/lib/templates/project/reactexternalapp/_p_/_m_/classes/UIBundleChangePassword.cls-meta.xml +1 -1
  49. package/lib/templates/project/reactexternalapp/_p_/_m_/classes/UIBundleForgotPassword.cls-meta.xml +1 -1
  50. package/lib/templates/project/reactexternalapp/_p_/_m_/classes/UIBundleLogin.cls-meta.xml +1 -1
  51. package/lib/templates/project/reactexternalapp/_p_/_m_/classes/UIBundleRegistration.cls-meta.xml +1 -1
  52. package/lib/templates/project/reactexternalapp/_p_/_m_/package.xml +1 -1
  53. package/lib/templates/project/reactexternalapp/package.json +1 -1
  54. package/lib/templates/project/reactexternalapp/scripts/org-setup.config.json +0 -1
  55. package/lib/templates/project/reactexternalapp/scripts/org-setup.mjs +528 -44
  56. package/lib/templates/project/reactexternalapp/sfdx-project.json +1 -1
  57. package/lib/templates/project/reactinternalapp/AGENT.md +3 -3
  58. package/lib/templates/project/reactinternalapp/CHANGELOG.md +428 -0
  59. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/package-lock.json +784 -2036
  60. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/package.json +5 -5
  61. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/reactinternalapp.uibundle-meta.xml +1 -0
  62. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/pages/AccountObjectDetailPage.tsx +7 -9
  63. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/pages/AccountSearch.tsx +7 -15
  64. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/api/objectSearchService.ts +14 -19
  65. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/NumericRangeFilter.tsx +9 -5
  66. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/SearchFilter.tsx +5 -3
  67. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/TextFilter.tsx +5 -3
  68. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/hooks/useAsyncData.ts +11 -4
  69. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/api/graphqlClient.ts +13 -13
  70. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/components/AgentforceConversationClient.tsx +40 -44
  71. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/components/alerts/status-alert.tsx +11 -8
  72. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/components/ui/input.tsx +1 -1
  73. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/hooks/useAsyncData.ts +67 -0
  74. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/pages/AccountObjectDetailPage.tsx +7 -9
  75. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/pages/AccountSearch.tsx +7 -15
  76. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/types/conversation.ts +9 -0
  77. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/types/globals.d.ts +13 -0
  78. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/tsconfig.json +4 -6
  79. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/vite-env.d.ts +0 -3
  80. package/lib/templates/project/reactinternalapp/_p_/_m_/applications/reactinternalapp.app-meta.xml +17 -0
  81. package/lib/templates/project/reactinternalapp/_p_/_m_/permissionsets/reactinternalapp_Access.permissionset-meta.xml +9 -0
  82. package/lib/templates/project/reactinternalapp/package.json +1 -1
  83. package/lib/templates/project/reactinternalapp/scripts/org-setup.config.json +6 -3
  84. package/lib/templates/project/reactinternalapp/scripts/org-setup.mjs +528 -44
  85. package/lib/templates/project/reactinternalapp/sfdx-project.json +1 -1
  86. package/lib/templates/uiBundles/reactbasic/package-lock.json +1040 -593
  87. package/lib/templates/uiBundles/reactbasic/package.json +3 -3
  88. package/lib/templates/uiBundles/reactbasic/src/api/graphqlClient.ts +13 -13
  89. package/lib/templates/uiBundles/reactbasic/src/components/alerts/status-alert.tsx +11 -8
  90. package/lib/templates/uiBundles/reactbasic/src/components/ui/input.tsx +1 -1
  91. package/lib/templates/uiBundles/reactbasic/src/hooks/useAsyncData.ts +67 -0
  92. package/lib/templates/uiBundles/reactbasic/tsconfig.json +4 -6
  93. package/lib/templates/uiBundles/reactbasic/vite-env.d.ts +0 -3
  94. package/lib/tsconfig.tsbuildinfo +1 -1
  95. package/lib/utils/lightningEmbedding.d.ts +12 -0
  96. package/lib/utils/lightningEmbedding.js +50 -0
  97. package/lib/utils/lightningEmbedding.js.map +1 -0
  98. package/lib/utils/types.d.ts +15 -6
  99. package/lib/utils/types.js +8 -5
  100. package/lib/utils/types.js.map +1 -1
  101. package/package.json +6 -6
  102. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/hooks/useCachedAsyncData.ts +0 -188
  103. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/layout/card-skeleton.tsx +0 -38
  104. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/layouts/authenticationRouteLayout.tsx +0 -21
  105. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/hooks/useCachedAsyncData.ts +0 -188
@@ -22,17 +22,18 @@
22
22
  * Permset assignment config (scripts/org-setup.config.json):
23
23
  * {
24
24
  * "permsetAssignments": {
25
- * "defaultAssignee": "currentUser",
26
25
  * "assignments": {
27
- * "Guest_Permset": { "assignee": "guest@mysite.example.com" },
26
+ * "My_Permset": { "assignee": "currentUser" },
27
+ * "Guest_Permset": { "assignee": "guestUser", "siteName": "mysite" },
28
28
  * "Internal_Only": { "assignee": "skip" }
29
29
  * }
30
30
  * }
31
31
  * }
32
- * Assignee values: "currentUser" (default), "skip", or a specific username.
32
+ * Assignee values: "currentUser", "skip", "guestUser" (requires siteName), or a specific username.
33
+ * Only permsets explicitly listed in assignments are assigned; unlisted permsets are skipped.
33
34
  */
34
35
 
35
- import { spawnSync } from 'node:child_process';
36
+ import { spawnSync, spawn as nodeSpawn } from 'node:child_process';
36
37
  import { resolve, dirname } from 'node:path';
37
38
  import { fileURLToPath } from 'node:url';
38
39
  import { readdirSync, existsSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';
@@ -97,9 +98,11 @@ function parseArgs() {
97
98
  skipLogin: false,
98
99
  skipDeploy: false,
99
100
  skipPermset: false,
101
+ skipRole: false,
100
102
  skipData: false,
101
103
  skipGraphql: false,
102
104
  skipUIBundleBuild: false,
105
+ skipSelfReg: false,
103
106
  skipDev: false,
104
107
  };
105
108
  for (let i = 0; i < args.length; i++) {
@@ -112,7 +115,9 @@ function parseArgs() {
112
115
  } else if (args[i] === '--skip-login') flags.skipLogin = true;
113
116
  else if (args[i] === '--skip-deploy') flags.skipDeploy = true;
114
117
  else if (args[i] === '--skip-permset') flags.skipPermset = true;
118
+ else if (args[i] === '--skip-role') flags.skipRole = true;
115
119
  else if (args[i] === '--skip-data') flags.skipData = true;
120
+ else if (args[i] === '--skip-self-reg') flags.skipSelfReg = true;
116
121
  else if (args[i] === '--skip-graphql') flags.skipGraphql = true;
117
122
  else if (args[i] === '--skip-ui-bundle-build') flags.skipUIBundleBuild = true;
118
123
  else if (args[i] === '--skip-dev') flags.skipDev = true;
@@ -144,15 +149,15 @@ Permset config (scripts/org-setup.config.json):
144
149
  Control per-permset assignment via a config file. Example:
145
150
  {
146
151
  "permsetAssignments": {
147
- "defaultAssignee": "currentUser",
148
152
  "assignments": {
149
- "Guest_Permset": { "assignee": "guest@mysite.example.com" },
153
+ "My_Permset": { "assignee": "currentUser" },
154
+ "Guest_Permset": { "assignee": "guestUser", "siteName": "mysite" },
150
155
  "Internal_Only": { "assignee": "skip" }
151
156
  }
152
157
  }
153
158
  }
154
- Assignee values: "currentUser" (default), "skip", or a specific username.
155
- Without this file, all discovered permsets are assigned to the current user.
159
+ Assignee values: "currentUser", "skip", "guestUser" (requires siteName), or a specific username.
160
+ Only permsets explicitly listed in assignments are assigned; unlisted permsets are skipped.
156
161
  `);
157
162
  process.exit(0);
158
163
  }
@@ -213,31 +218,33 @@ function discoverPermissionSetNames() {
213
218
  * Config shape:
214
219
  * {
215
220
  * "permsetAssignments": {
216
- * "defaultAssignee": "currentUser", // "currentUser" (default) or "skip"
217
221
  * "assignments": {
218
- * "My_Guest_Permset": { "assignee": "guest@mysite.example.com" },
222
+ * "My_Permset": { "assignee": "currentUser" },
223
+ * "My_Guest_Permset": { "assignee": "guestUser", "siteName": "mysite" },
219
224
  * "Internal_Only": { "assignee": "skip" }
220
225
  * }
221
226
  * }
222
227
  * }
223
228
  *
224
229
  * Assignee values:
225
- * "currentUser" — assign to the user running the script (default)
230
+ * "currentUser" — assign to the user running the script
226
231
  * "skip" — do not assign this permset
232
+ * "guestUser" — resolve the site guest user automatically (requires siteName)
227
233
  * "<username>" — assign to a specific user via --on-behalf-of
228
234
  *
229
- * Returns { defaultAssignee: string, assignments: Record<string, { assignee: string }> }
235
+ * Only permsets explicitly listed in assignments are assigned; unlisted permsets are skipped.
236
+ *
237
+ * Returns { assignments: Record<string, { assignee: string, siteName?: string }> }
230
238
  */
231
239
  function loadPermsetConfig() {
232
240
  const configPath = resolve(__dirname, 'org-setup.config.json');
233
- const defaults = { defaultAssignee: 'currentUser', assignments: {} };
241
+ const defaults = { assignments: {} };
234
242
  if (!existsSync(configPath)) return defaults;
235
243
  try {
236
244
  const raw = JSON.parse(readFileSync(configPath, 'utf8'));
237
245
  const section = raw?.permsetAssignments;
238
246
  if (!section) return defaults;
239
247
  return {
240
- defaultAssignee: section.defaultAssignee || 'currentUser',
241
248
  assignments: section.assignments || {},
242
249
  };
243
250
  } catch (err) {
@@ -246,10 +253,375 @@ function loadPermsetConfig() {
246
253
  }
247
254
  }
248
255
 
249
- /** Resolve the effective assignee for a given permset name. */
250
- function resolveAssignee(permsetName, permsetConfig) {
256
+ /** Resolve the effective assignment config for a given permset name. */
257
+ function resolveAssignment(permsetName, permsetConfig) {
251
258
  const override = permsetConfig.assignments[permsetName];
252
- return override?.assignee || permsetConfig.defaultAssignee;
259
+ if (!override) return { assignee: 'skip' };
260
+ return { assignee: override.assignee || 'skip', siteName: override.siteName };
261
+ }
262
+
263
+ /**
264
+ * Load role assignment config from org-setup.config.json.
265
+ *
266
+ * Config shape:
267
+ * { "role": { "assignee": "currentUser", "roleName": "Admin" } }
268
+ *
269
+ * Returns null if no "role" section exists in config (the step is hidden).
270
+ */
271
+ function loadRoleConfig() {
272
+ const configPath = resolve(__dirname, 'org-setup.config.json');
273
+ if (!existsSync(configPath)) return null;
274
+ try {
275
+ const raw = JSON.parse(readFileSync(configPath, 'utf8'));
276
+ const section = raw?.role;
277
+ if (!section) return null;
278
+ return {
279
+ assignee: section.assignee || 'currentUser',
280
+ roleName: section.roleName || null,
281
+ };
282
+ } catch {
283
+ return null;
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Load self-registration config from org-setup.config.json.
289
+ *
290
+ * Config shape:
291
+ * {
292
+ * "selfRegistration": {
293
+ * "siteName": "myapp",
294
+ * "selfRegProfile": "myapp Profile",
295
+ * "accountName": "My Self-Reg Account"
296
+ * }
297
+ * }
298
+ *
299
+ * Returns null if no "selfRegistration" section exists in config (the step is hidden).
300
+ */
301
+ function loadSelfRegConfig() {
302
+ const configPath = resolve(__dirname, 'org-setup.config.json');
303
+ if (!existsSync(configPath)) return null;
304
+ try {
305
+ const raw = JSON.parse(readFileSync(configPath, 'utf8'));
306
+ const section = raw?.selfRegistration;
307
+ if (!section) return null;
308
+ return {
309
+ siteName: section.siteName || null,
310
+ selfRegProfile: section.selfRegProfile || null,
311
+ accountName: section.accountName || null,
312
+ };
313
+ } catch {
314
+ return null;
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Ensure the self-registration profile is listed in networkMemberGroups.
320
+ * This must happen BEFORE the initial deploy so that the profile is a recognised
321
+ * site member when subsequent steps (selfRegProfile, selfRegistration=true) are deployed.
322
+ */
323
+ function ensureNetworkMemberProfile(selfRegConfig) {
324
+ const { siteName, selfRegProfile } = selfRegConfig;
325
+ if (!siteName || !selfRegProfile) return;
326
+
327
+ const networkXmlPath = resolve(SFDX_SOURCE, 'networks', `${siteName}.network-meta.xml`);
328
+ if (!existsSync(networkXmlPath)) {
329
+ console.log(` Network metadata not found: ${networkXmlPath}; skipping member group update.`);
330
+ return;
331
+ }
332
+ const xml = readFileSync(networkXmlPath, 'utf8');
333
+
334
+ // Check if profile is already in networkMemberGroups
335
+ const profileEscaped = selfRegProfile.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
336
+ const profileRegex = new RegExp(`<profile>\\s*${profileEscaped}\\s*</profile>`);
337
+ if (profileRegex.test(xml)) {
338
+ console.log(` Profile "${selfRegProfile}" already in networkMemberGroups; no update needed.`);
339
+ return;
340
+ }
341
+
342
+ // Add the profile to networkMemberGroups
343
+ const updatedXml = xml.replace(
344
+ /(<networkMemberGroups>)/,
345
+ `$1\n <profile>${selfRegProfile}</profile>`
346
+ );
347
+ writeFileSync(networkXmlPath, updatedXml);
348
+ console.log(` Added profile "${selfRegProfile}" to networkMemberGroups in ${siteName}.network-meta.xml`);
349
+ }
350
+
351
+ /**
352
+ * Enable self-registration for an Experience Cloud network.
353
+ *
354
+ * 1. Modify the network metadata XML to set selfRegistration=true and add selfRegProfile.
355
+ * 2. Re-deploy the modified network metadata.
356
+ * 3. Create an Account record (idempotent).
357
+ * 4. Create a NetworkSelfRegistration record linking the Account to the Network (idempotent).
358
+ */
359
+ function enableSelfRegistration(selfRegConfig, targetOrg) {
360
+ const { siteName, selfRegProfile, accountName } = selfRegConfig;
361
+
362
+ // 1. Modify network metadata XML
363
+ const networkXmlPath = resolve(SFDX_SOURCE, 'networks', `${siteName}.network-meta.xml`);
364
+ if (!existsSync(networkXmlPath)) {
365
+ console.error(` Network metadata not found: ${networkXmlPath}`);
366
+ return;
367
+ }
368
+ const xml = readFileSync(networkXmlPath, 'utf8');
369
+
370
+ // Skip network modification and deploy if self-registration is already configured
371
+ const alreadyEnabled = /<selfRegistration>true<\/selfRegistration>/.test(xml);
372
+ const alreadyHasProfile = /<selfRegProfile>/.test(xml);
373
+ if (alreadyEnabled || alreadyHasProfile) {
374
+ console.log(` Network "${siteName}" already has self-registration configured; skipping metadata update and deploy.`);
375
+ } else {
376
+ // Set selfRegistration to true and add selfRegProfile
377
+ let updatedXml = xml.replace(
378
+ /<selfRegistration>false<\/selfRegistration>/,
379
+ '<selfRegistration>true</selfRegistration>'
380
+ );
381
+ updatedXml = updatedXml.replace(
382
+ /(\s*)(<selfRegistration>)/,
383
+ `$1<selfRegProfile>${selfRegProfile}</selfRegProfile>\n$1$2`
384
+ );
385
+
386
+ writeFileSync(networkXmlPath, updatedXml);
387
+ console.log(` Updated ${siteName}.network-meta.xml: selfRegistration=true, selfRegProfile=${selfRegProfile}`);
388
+
389
+ // Re-deploy only the network file
390
+ const deployResult = spawnSync('sf', [
391
+ 'project', 'deploy', 'start',
392
+ '--target-org', targetOrg,
393
+ '--source-dir', networkXmlPath,
394
+ ], { cwd: ROOT, stdio: 'inherit', shell: true, timeout: 120000 });
395
+ if (deployResult.status !== 0) {
396
+ console.error(' Failed to deploy updated network metadata.');
397
+ process.exit(deployResult.status ?? 1);
398
+ }
399
+ }
400
+
401
+ // 3. Create Account (idempotent)
402
+ const acctQuery = `SELECT Id FROM Account WHERE Name = '${accountName.replace(/'/g, "\\'")}' LIMIT 1`;
403
+ const acctQueryResult = spawnSync('sf', [
404
+ 'data', 'query',
405
+ '--query', acctQuery,
406
+ '--target-org', targetOrg,
407
+ '--json',
408
+ ], { cwd: ROOT, encoding: 'utf8' });
409
+ let accountId = null;
410
+ if (acctQueryResult.status === 0) {
411
+ try {
412
+ const json = JSON.parse(acctQueryResult.stdout);
413
+ accountId = json.result?.records?.[0]?.Id || null;
414
+ } catch { /* proceed to create */ }
415
+ }
416
+ if (accountId) {
417
+ console.log(` Account "${accountName}" already exists (${accountId}); skipping creation.`);
418
+ } else {
419
+ const createResult = spawnSync('sf', [
420
+ 'data', 'create', 'record',
421
+ '--sobject', 'Account',
422
+ '--values', `Name='${accountName}'`,
423
+ '--target-org', targetOrg,
424
+ '--json',
425
+ ], { cwd: ROOT, encoding: 'utf8' });
426
+ if (createResult.status !== 0) {
427
+ console.error(` Failed to create Account "${accountName}".`);
428
+ if (createResult.stderr) console.error(createResult.stderr);
429
+ return;
430
+ }
431
+ try {
432
+ const json = JSON.parse(createResult.stdout);
433
+ accountId = json.result?.id;
434
+ console.log(` Created Account "${accountName}" (${accountId}).`);
435
+ } catch {
436
+ console.error(' Failed to parse Account creation result.');
437
+ return;
438
+ }
439
+ }
440
+
441
+ // 4. Query Network Id
442
+ const netQuery = `SELECT Id FROM Network WHERE Name = '${siteName}'`;
443
+ const netResult = spawnSync('sf', [
444
+ 'data', 'query',
445
+ '--query', netQuery,
446
+ '--target-org', targetOrg,
447
+ '--json',
448
+ ], { cwd: ROOT, encoding: 'utf8' });
449
+ let networkId = null;
450
+ if (netResult.status === 0) {
451
+ try {
452
+ const json = JSON.parse(netResult.stdout);
453
+ networkId = json.result?.records?.[0]?.Id || null;
454
+ } catch { /* fall through */ }
455
+ }
456
+ if (!networkId) {
457
+ console.error(` Could not find Network "${siteName}" in org.`);
458
+ return;
459
+ }
460
+ console.log(` Found Network "${siteName}" (${networkId}).`);
461
+
462
+ // 5. Create NetworkSelfRegistration (idempotent)
463
+ const nsrQuery = `SELECT Id FROM NetworkSelfRegistration WHERE NetworkId = '${networkId}'`;
464
+ const nsrResult = spawnSync('sf', [
465
+ 'data', 'query',
466
+ '--query', nsrQuery,
467
+ '--target-org', targetOrg,
468
+ '--json',
469
+ ], { cwd: ROOT, encoding: 'utf8' });
470
+ let nsrExists = false;
471
+ if (nsrResult.status === 0) {
472
+ try {
473
+ const json = JSON.parse(nsrResult.stdout);
474
+ nsrExists = (json.result?.records?.length || 0) > 0;
475
+ } catch { /* proceed to create */ }
476
+ }
477
+ if (nsrExists) {
478
+ console.log(' NetworkSelfRegistration record already exists; skipping.');
479
+ } else {
480
+ const tmpApex = resolve(ROOT, '.tmp-setup-selfreg.apex');
481
+ const apex = [
482
+ `Account acct = [SELECT Id FROM Account WHERE Id = '${accountId}' LIMIT 1];`,
483
+ `NetworkSelfRegistration nsr = new NetworkSelfRegistration();`,
484
+ `nsr.AccountId = acct.Id;`,
485
+ `nsr.NetworkId = '${networkId}';`,
486
+ `insert nsr;`,
487
+ `System.debug('NSR_CREATED:' + nsr.Id);`,
488
+ ].join('\n');
489
+ writeFileSync(tmpApex, apex);
490
+ const apexResult = spawnSync('sf', [
491
+ 'apex', 'run', '--target-org', targetOrg, '--file', tmpApex,
492
+ ], { cwd: ROOT, stdio: 'pipe', shell: true, timeout: 60000 });
493
+ const apexOut = apexResult.stdout?.toString() || '';
494
+ if (existsSync(tmpApex)) unlinkSync(tmpApex);
495
+ if (apexResult.status !== 0 && !apexOut.includes('Compiled successfully')) {
496
+ console.error(' Failed to create NetworkSelfRegistration record.');
497
+ process.stderr.write(apexResult.stderr?.toString() || apexOut);
498
+ return;
499
+ }
500
+ const nsrMatch = apexOut.match(/NSR_CREATED:(\w+)/);
501
+ if (nsrMatch) {
502
+ console.log(` Created NetworkSelfRegistration (${nsrMatch[1]}).`);
503
+ } else {
504
+ console.log(' NetworkSelfRegistration creation executed.');
505
+ }
506
+ }
507
+ }
508
+
509
+ /**
510
+ * Assign a role to the current user so that Experience Cloud self-registration
511
+ * works correctly.
512
+ */
513
+ function assignRoleToCurrentUser(roleName, targetOrg) {
514
+ const roleQuery = `SELECT Id FROM UserRole WHERE Name = '${roleName}'`;
515
+ const roleResult = spawnSync('sf', [
516
+ 'data', 'query',
517
+ '--query', roleQuery,
518
+ '--target-org', targetOrg,
519
+ '--json',
520
+ ], { cwd: ROOT, encoding: 'utf8' });
521
+ if (roleResult.status !== 0) {
522
+ console.error(` Failed to query role "${roleName}" in org.`);
523
+ if (roleResult.stderr) console.error(roleResult.stderr);
524
+ return;
525
+ }
526
+ let roleId;
527
+ try {
528
+ const json = JSON.parse(roleResult.stdout);
529
+ const records = json.result?.records;
530
+ if (!records || records.length === 0) {
531
+ console.error(` Role "${roleName}" not found in org; skipping.`);
532
+ return;
533
+ }
534
+ roleId = records[0].Id;
535
+ } catch {
536
+ console.error(` Failed to parse role query result for "${roleName}".`);
537
+ return;
538
+ }
539
+
540
+ const orgResult = spawnSync('sf', [
541
+ 'org', 'display',
542
+ '--target-org', targetOrg,
543
+ '--json',
544
+ ], { cwd: ROOT, encoding: 'utf8' });
545
+ if (orgResult.status !== 0) {
546
+ console.error(' Failed to resolve current user from org.');
547
+ return;
548
+ }
549
+ let username;
550
+ try {
551
+ const json = JSON.parse(orgResult.stdout);
552
+ username = json.result?.username;
553
+ if (!username) {
554
+ console.error(' Could not determine current username from org display.');
555
+ return;
556
+ }
557
+ } catch {
558
+ console.error(' Failed to parse org display result.');
559
+ return;
560
+ }
561
+
562
+ const userQuery = `SELECT Id, UserRoleId FROM User WHERE Username = '${username}'`;
563
+ const userResult = spawnSync('sf', [
564
+ 'data', 'query',
565
+ '--query', userQuery,
566
+ '--target-org', targetOrg,
567
+ '--json',
568
+ ], { cwd: ROOT, encoding: 'utf8' });
569
+ if (userResult.status === 0) {
570
+ try {
571
+ const json = JSON.parse(userResult.stdout);
572
+ const userRecord = json.result?.records?.[0];
573
+ if (userRecord?.UserRoleId) {
574
+ console.log(` User ${username} already has a role assigned; skipping to avoid overriding.`);
575
+ return;
576
+ }
577
+ } catch { /* continue */ }
578
+ }
579
+
580
+ const updateResult = spawnSync('sf', [
581
+ 'data', 'update', 'record',
582
+ '--sobject', 'User',
583
+ '--where', `Username='${username}'`,
584
+ '--values', `UserRoleId='${roleId}'`,
585
+ '--target-org', targetOrg,
586
+ '--json',
587
+ ], { cwd: ROOT, encoding: 'utf8' });
588
+ if (updateResult.status === 0) {
589
+ console.log(` Role "${roleName}" assigned to ${username}.`);
590
+ } else {
591
+ const out = (updateResult.stderr?.toString() || '') + (updateResult.stdout?.toString() || '');
592
+ console.error(` Failed to assign role "${roleName}" to ${username}.`);
593
+ if (out) console.error(out);
594
+ }
595
+ }
596
+
597
+ /**
598
+ * Query the org for a guest user whose profile name matches the given site name.
599
+ */
600
+ function resolveGuestUsername(siteName, targetOrg) {
601
+ const query = `SELECT Username FROM User WHERE Profile.Name LIKE '%${siteName}%' AND UserType = 'Guest'`;
602
+ const result = spawnSync('sf', [
603
+ 'data', 'query',
604
+ '--query', query,
605
+ '--target-org', targetOrg,
606
+ '--json',
607
+ ], { cwd: ROOT, encoding: 'utf8' });
608
+ if (result.status !== 0) {
609
+ console.error(` Failed to query guest user for site "${siteName}".`);
610
+ if (result.stderr) console.error(result.stderr);
611
+ return null;
612
+ }
613
+ try {
614
+ const json = JSON.parse(result.stdout);
615
+ const records = json.result?.records;
616
+ if (!records || records.length === 0) {
617
+ console.error(` No guest user found for site "${siteName}".`);
618
+ return null;
619
+ }
620
+ return records[0].Username;
621
+ } catch {
622
+ console.error(` Failed to parse guest user query result for site "${siteName}".`);
623
+ return null;
624
+ }
253
625
  }
254
626
 
255
627
  function isOrgConnected(targetOrg) {
@@ -312,6 +684,22 @@ async function promptSteps(steps) {
312
684
  const CYAN = '\x1B[36m';
313
685
  const GREEN = '\x1B[32m';
314
686
 
687
+ /** Strip ANSI escape sequences to get visible character count. */
688
+ function visibleLength(str) {
689
+ return str.replace(/\x1B\[[0-9;]*m/g, '').length;
690
+ }
691
+
692
+ /** Count how many terminal rows a set of lines occupies (accounting for wrapping). */
693
+ function terminalRows(lines) {
694
+ const cols = process.stdout.columns || 80;
695
+ let rows = 0;
696
+ for (const line of lines) {
697
+ const len = visibleLength(line);
698
+ rows += len === 0 ? 1 : Math.ceil(len / cols);
699
+ }
700
+ return rows;
701
+ }
702
+
315
703
  function render() {
316
704
  return steps.map((s, i) => {
317
705
  const ptr = i === cursor ? `${CYAN}❯${RST}` : ' ';
@@ -321,17 +709,23 @@ async function promptSteps(steps) {
321
709
  });
322
710
  }
323
711
 
712
+ let prevRows = 0;
713
+
324
714
  return new Promise((resolve) => {
325
715
  process.stdin.setRawMode(true);
326
716
  process.stdin.resume();
327
717
  process.stdin.setEncoding('utf8');
328
718
  process.stdout.write('\x1B[?25l');
329
719
  console.log('\nSelect steps (↑↓ move, space toggle, a all, enter confirm):\n');
330
- process.stdout.write(render().join('\n') + '\n');
720
+ const initialLines = render();
721
+ prevRows = terminalRows(initialLines);
722
+ process.stdout.write(initialLines.join('\n') + '\n');
331
723
 
332
724
  function redraw() {
333
- process.stdout.write(`\x1B[${steps.length}A`);
334
- for (const line of render()) process.stdout.write(`\x1B[2K${line}\n`);
725
+ process.stdout.write(`\x1B[${prevRows}A`);
726
+ const lines = render();
727
+ for (const line of lines) process.stdout.write(`\x1B[2K${line}\n`);
728
+ prevRows = terminalRows(lines);
335
729
  }
336
730
 
337
731
  process.stdin.on('data', (key) => {
@@ -388,6 +782,37 @@ function run(name, cmd, args, opts = {}) {
388
782
  return result;
389
783
  }
390
784
 
785
+ /** Promise-based spawn for parallel execution. Always uses stdio: 'pipe'. */
786
+ function spawnAsync(cmd, args, opts = {}) {
787
+ return new Promise((resolve, reject) => {
788
+ const proc = nodeSpawn(cmd, args, {
789
+ cwd: opts.cwd || ROOT,
790
+ stdio: 'pipe',
791
+ shell: true,
792
+ ...(opts.timeout && { timeout: opts.timeout }),
793
+ });
794
+ let stdout = '';
795
+ let stderr = '';
796
+ proc.stdout.on('data', (d) => { stdout += d.toString(); });
797
+ proc.stderr.on('data', (d) => { stderr += d.toString(); });
798
+ proc.on('close', (code) => resolve({ status: code, stdout, stderr }));
799
+ proc.on('error', reject);
800
+ });
801
+ }
802
+
803
+ /** Async version of run() for parallel steps. Captures output and prints on failure. */
804
+ async function runAsync(name, cmd, args, opts = {}) {
805
+ const { cwd = ROOT, optional = false } = opts;
806
+ const result = await spawnAsync(cmd, args, { cwd, ...(opts.timeout && { timeout: opts.timeout }) });
807
+ if (result.status !== 0 && !optional) {
808
+ if (result.stdout) process.stdout.write(result.stdout);
809
+ if (result.stderr) process.stderr.write(result.stderr);
810
+ console.error(`\nSetup failed at step: ${name}`);
811
+ process.exit(result.status ?? 1);
812
+ }
813
+ return result;
814
+ }
815
+
391
816
  async function main() {
392
817
  // Ensure .gitignore files exist (npm strips them from published packages).
393
818
  const gitignoreTemplates = loadGitignoreTemplates();
@@ -410,6 +835,8 @@ async function main() {
410
835
  skipLogin: argSkipLogin,
411
836
  skipDeploy: argSkipDeploy,
412
837
  skipPermset: argSkipPermset,
838
+ skipRole: argSkipRole,
839
+ skipSelfReg: argSkipSelfReg,
413
840
  skipData: argSkipData,
414
841
  skipGraphql: argSkipGraphql,
415
842
  skipUIBundleBuild: argSkipUIBundleBuild,
@@ -426,12 +853,18 @@ async function main() {
426
853
  : `Permset — assign ${permsetNames.length} permission sets`;
427
854
 
428
855
  const hasDataPlan = existsSync(DATA_PLAN) && existsSync(DATA_DIR);
856
+ const roleConfig = loadRoleConfig();
857
+ const hasRoleConfig = roleConfig !== null;
858
+ const selfRegConfig = loadSelfRegConfig();
859
+ const hasSelfRegConfig = selfRegConfig !== null;
429
860
 
430
861
  const stepDefs = [
431
862
  { key: 'login', label: 'Login — org authentication', enabled: !argSkipLogin, available: true },
432
863
  { key: 'uiBundleBuild', label: 'UI Bundle Build — npm install + build (pre-deploy)', enabled: !argSkipUIBundleBuild, available: true },
433
864
  { key: 'deploy', label: 'Deploy — sf project deploy start', enabled: !argSkipDeploy, available: true },
434
865
  { key: 'permset', label: permsetStepLabel, enabled: !argSkipPermset, available: true },
866
+ { key: 'role', label: `Role — assign "${roleConfig?.roleName ?? '?'}" to current user`, enabled: !argSkipRole && hasRoleConfig, available: hasRoleConfig },
867
+ { key: 'selfReg', label: `Self-Registration — enable for "${selfRegConfig?.siteName ?? '?'}"`, enabled: !argSkipSelfReg && hasSelfRegConfig, available: hasSelfRegConfig },
435
868
  { key: 'data', label: 'Data — delete + import records via Apex', enabled: !argSkipData && hasDataPlan, available: hasDataPlan },
436
869
  { key: 'graphql', label: 'GraphQL — schema introspect + codegen', enabled: !argSkipGraphql, available: true },
437
870
  { key: 'dev', label: 'Dev — launch dev server', enabled: !argSkipDev, available: true },
@@ -447,6 +880,8 @@ async function main() {
447
880
  const skipUIBundleBuild = !on.uiBundleBuild;
448
881
  const skipDeploy = !on.deploy;
449
882
  const skipPermset = !on.permset;
883
+ const skipRole = !on.role;
884
+ const skipSelfReg = !on.selfReg;
450
885
  const skipData = !on.data;
451
886
  const skipGraphql = !on.graphql;
452
887
  const skipDev = !on.dev;
@@ -457,10 +892,12 @@ async function main() {
457
892
 
458
893
  console.log('Setup — target org:', targetOrg, '| UI bundle:', uiBundleDir ?? '(none)');
459
894
  console.log(
460
- 'Steps: login=%s deploy=%s permset=%s data=%s graphql=%s uiBundle=%s dev=%s',
895
+ 'Steps: login=%s deploy=%s permset=%s role=%s selfReg=%s data=%s graphql=%s uiBundle=%s dev=%s',
461
896
  !skipLogin,
462
897
  !skipDeploy,
463
898
  !skipPermset,
899
+ !skipRole,
900
+ !skipSelfReg,
464
901
  doData,
465
902
  !skipGraphql,
466
903
  !skipUIBundleBuild,
@@ -476,6 +913,13 @@ async function main() {
476
913
  }
477
914
  }
478
915
 
916
+ // Ensure the self-reg profile is in networkMemberGroups before deploy so that
917
+ // subsequent selfRegProfile / selfRegistration updates don't fail.
918
+ if (!skipDeploy && selfRegConfig) {
919
+ console.log('\n--- Ensure network member profile (pre-deploy) ---');
920
+ ensureNetworkMemberProfile(selfRegConfig);
921
+ }
922
+
479
923
  // Build all UI Bundles before deploy so dist exists for entity deployment
480
924
  if (!skipDeploy && !skipUIBundleBuild) {
481
925
  const allUIBundleDirs = discoverAllUIBundleDirs(uiBundleName);
@@ -499,42 +943,84 @@ async function main() {
499
943
  console.log('No permission sets found under permissionsets/ and none passed via --permset-name; skipping.');
500
944
  } else {
501
945
  console.log('\n--- Assign permission sets ---');
946
+
947
+ // Resolve assignments (guest user lookups etc.) then run all sf assign calls in parallel.
948
+ const assignmentJobs = [];
502
949
  for (const permsetName of permsetNames) {
503
- const assignee = resolveAssignee(permsetName, permsetConfig);
504
- if (assignee === 'skip') {
950
+ const assignment = resolveAssignment(permsetName, permsetConfig);
951
+ if (assignment.assignee === 'skip') {
505
952
  console.log(`Permission set "${permsetName}" — skipped (config).`);
506
953
  continue;
507
954
  }
955
+ let effectiveUsername = null;
956
+ if (assignment.assignee === 'guestUser') {
957
+ if (!assignment.siteName) {
958
+ console.error(`Permission set "${permsetName}" — assignee is "guestUser" but no "siteName" configured; skipping.`);
959
+ continue;
960
+ }
961
+ effectiveUsername = resolveGuestUsername(assignment.siteName, targetOrg);
962
+ if (!effectiveUsername) {
963
+ console.error(`Permission set "${permsetName}" — could not resolve guest user for site "${assignment.siteName}"; skipping.`);
964
+ continue;
965
+ }
966
+ console.log(` Resolved guest user for site "${assignment.siteName}": ${effectiveUsername}`);
967
+ } else if (assignment.assignee !== 'currentUser') {
968
+ effectiveUsername = assignment.assignee;
969
+ }
970
+ assignmentJobs.push({ permsetName, effectiveUsername });
971
+ }
972
+
973
+ // Run all permset assignment calls in parallel.
974
+ const assignResults = await Promise.all(assignmentJobs.map(async ({ permsetName, effectiveUsername }) => {
508
975
  const sfArgs = ['org', 'assign', 'permset', '--name', permsetName, '--target-org', targetOrg];
509
- if (assignee !== 'currentUser') {
510
- sfArgs.push('--on-behalf-of', assignee);
976
+ if (effectiveUsername) {
977
+ sfArgs.push('--on-behalf-of', effectiveUsername);
511
978
  }
512
- const assigneeLabel = assignee === 'currentUser' ? 'current user' : assignee;
513
- const permsetResult = spawnSync('sf', sfArgs, {
514
- cwd: ROOT,
515
- stdio: 'pipe',
516
- shell: true,
517
- });
518
- if (permsetResult.status === 0) {
979
+ const assigneeLabel = effectiveUsername || 'current user';
980
+ const result = await spawnAsync('sf', sfArgs);
981
+ return { permsetName, assigneeLabel, result };
982
+ }));
983
+
984
+ for (const { permsetName, assigneeLabel, result } of assignResults) {
985
+ if (result.status === 0) {
519
986
  console.log(`Permission set "${permsetName}" assigned to ${assigneeLabel}.`);
520
987
  } else {
521
- const out =
522
- (permsetResult.stderr?.toString() || '') + (permsetResult.stdout?.toString() || '');
988
+ const out = (result.stderr || '') + (result.stdout || '');
523
989
  if (out.includes('Duplicate') && out.includes('PermissionSet')) {
524
990
  console.log(`Permission set "${permsetName}" already assigned to ${assigneeLabel}; skipping.`);
525
991
  } else if (out.includes('not found') && out.includes('target org')) {
526
992
  console.log(`Permission set "${permsetName}" not in org; skipping.`);
527
993
  } else {
528
- process.stdout.write(permsetResult.stdout?.toString() || '');
529
- process.stderr.write(permsetResult.stderr?.toString() || '');
994
+ if (result.stdout) process.stdout.write(result.stdout);
995
+ if (result.stderr) process.stderr.write(result.stderr);
530
996
  console.error(`\nSetup failed at step: Assign permission set (${permsetName})`);
531
- process.exit(permsetResult.status ?? 1);
997
+ process.exit(result.status ?? 1);
532
998
  }
533
999
  }
534
1000
  }
535
1001
  }
536
1002
  }
537
1003
 
1004
+ if (!skipRole) {
1005
+ console.log('\n--- Assign role ---');
1006
+ if (roleConfig?.assignee !== 'currentUser') {
1007
+ console.error(`Role assignee "${roleConfig?.assignee}" is not supported; only "currentUser" is allowed. Skipping.`);
1008
+ } else if (!roleConfig?.roleName) {
1009
+ console.error('Role step enabled but no "roleName" specified in org-setup.config.json; skipping.');
1010
+ } else {
1011
+ assignRoleToCurrentUser(roleConfig.roleName, targetOrg);
1012
+ }
1013
+ }
1014
+
1015
+ if (!skipSelfReg) {
1016
+ console.log('\n--- Enable self-registration ---');
1017
+ if (!selfRegConfig?.siteName || !selfRegConfig?.selfRegProfile || !selfRegConfig?.accountName) {
1018
+ console.error('Self-registration config is incomplete (need siteName, selfRegProfile, accountName); skipping.');
1019
+ } else {
1020
+ enableSelfRegistration(selfRegConfig, targetOrg);
1021
+ }
1022
+ }
1023
+
538
1024
  if (doData) {
539
1025
  // Prepare data for uniqueness (run before import so repeat imports don't conflict)
540
1026
  const prepareScript = resolve(__dirname, 'prepare-import-unique-fields.js');
@@ -652,17 +1138,15 @@ async function main() {
652
1138
  if (existsSync(tmpApex)) unlinkSync(tmpApex);
653
1139
  }
654
1140
 
655
- if (!skipGraphql || !skipUIBundleBuild) {
656
- run('UI Bundle npm install', 'npm', ['install'], { cwd: uiBundleDir });
657
- }
658
-
659
1141
  if (!skipGraphql) {
1142
+ run('UI Bundle npm install', 'npm', ['install'], { cwd: uiBundleDir });
660
1143
  run('Set default org for schema', 'sf', ['config', 'set', 'target-org', targetOrg, '--global']);
661
1144
  run('GraphQL schema (introspect)', 'npm', ['run', 'graphql:schema'], { cwd: uiBundleDir });
662
1145
  run('GraphQL codegen', 'npm', ['run', 'graphql:codegen'], { cwd: uiBundleDir });
663
- }
664
-
665
- if (!skipUIBundleBuild) {
1146
+ run('UI Bundle build (post-codegen)', 'npm', ['run', 'build'], { cwd: uiBundleDir });
1147
+ } else if (!skipUIBundleBuild && skipDeploy) {
1148
+ // Only build here if the pre-deploy build didn't already run
1149
+ run('UI Bundle npm install', 'npm', ['install'], { cwd: uiBundleDir });
666
1150
  run('UI Bundle build', 'npm', ['run', 'build'], { cwd: uiBundleDir });
667
1151
  }
668
1152