@salesforce/templates 66.10.2 → 66.10.4
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/lib/generators/uiBundleGenerator.d.ts +1 -0
- package/lib/generators/uiBundleGenerator.js +13 -0
- package/lib/generators/uiBundleGenerator.js.map +1 -1
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/package-lock.json +646 -812
- package/lib/templates/project/reactinternalapp/CHANGELOG.md +186 -0
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/package-lock.json +784 -996
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/package.json +4 -3
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/scripts/get-graphql-schema.mjs +8 -5
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/api/graphqlClient.ts +24 -8
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/components/AgentforceConversationClient.tsx +13 -1
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/pages/AccountObjectDetailPage.tsx +4 -2
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/types/conversation.ts +1 -0
- package/lib/templates/project/reactinternalapp/_p_/_m_/permissionsets/reactinternalapp_Access.permissionset-meta.xml +4 -0
- package/lib/templates/project/reactinternalapp/package.json +5 -1
- package/lib/templates/project/reactinternalapp/scripts/org-setup-config-schema.mjs +96 -0
- package/lib/templates/project/reactinternalapp/scripts/org-setup.mjs +410 -177
- package/lib/templates/project/reactinternalapp/scripts/validate-org-setup-config.mjs +38 -0
- package/lib/templates/uiBundles/angularbasic/.forceignore +15 -0
- package/lib/templates/uiBundles/angularbasic/.postcssrc.json +5 -0
- package/lib/templates/uiBundles/angularbasic/.prettierignore +8 -0
- package/lib/templates/uiBundles/angularbasic/.prettierrc +12 -0
- package/lib/templates/uiBundles/angularbasic/CHANGELOG.md +4 -0
- package/lib/templates/uiBundles/angularbasic/README.md +85 -0
- package/lib/templates/uiBundles/angularbasic/_uibundle.uibundle-meta.xml +7 -0
- package/lib/templates/uiBundles/angularbasic/angular.json +94 -0
- package/lib/templates/uiBundles/angularbasic/esbuild/api-version.mjs +17 -0
- package/lib/templates/uiBundles/angularbasic/eslint.config.js +56 -0
- package/lib/templates/uiBundles/angularbasic/middleware/html.mjs +3 -0
- package/lib/templates/uiBundles/angularbasic/middleware/proxy.mjs +12 -0
- package/lib/templates/uiBundles/angularbasic/package-lock.json +14070 -0
- package/lib/templates/uiBundles/angularbasic/package.json +52 -0
- package/lib/templates/uiBundles/angularbasic/playwright.config.ts +25 -0
- package/lib/templates/uiBundles/angularbasic/public/favicon.ico +0 -0
- package/lib/templates/uiBundles/angularbasic/scripts/rewrite-e2e-assets.mjs +30 -0
- package/lib/templates/uiBundles/angularbasic/src/app/api/graphql-client.service.spec.ts +154 -0
- package/lib/templates/uiBundles/angularbasic/src/app/api/graphql-client.service.ts +51 -0
- package/lib/templates/uiBundles/angularbasic/src/app/app.config.ts +8 -0
- package/lib/templates/uiBundles/angularbasic/src/app/app.css +0 -0
- package/lib/templates/uiBundles/angularbasic/src/app/app.html +1 -0
- package/lib/templates/uiBundles/angularbasic/src/app/app.routes.ts +21 -0
- package/lib/templates/uiBundles/angularbasic/src/app/app.spec.ts +18 -0
- package/lib/templates/uiBundles/angularbasic/src/app/app.ts +10 -0
- package/lib/templates/uiBundles/angularbasic/src/app/layout/app-layout.component.html +50 -0
- package/lib/templates/uiBundles/angularbasic/src/app/layout/app-layout.component.spec.ts +63 -0
- package/lib/templates/uiBundles/angularbasic/src/app/layout/app-layout.component.ts +21 -0
- package/lib/templates/uiBundles/angularbasic/src/app/pages/home/home.component.html +6 -0
- package/lib/templates/uiBundles/angularbasic/src/app/pages/home/home.component.spec.ts +23 -0
- package/lib/templates/uiBundles/angularbasic/src/app/pages/home/home.component.ts +7 -0
- package/lib/templates/uiBundles/angularbasic/src/app/pages/not-found/not-found.component.html +12 -0
- package/lib/templates/uiBundles/angularbasic/src/app/pages/not-found/not-found.component.spec.ts +33 -0
- package/lib/templates/uiBundles/angularbasic/src/app/pages/not-found/not-found.component.ts +9 -0
- package/lib/templates/uiBundles/angularbasic/src/index.html +13 -0
- package/lib/templates/uiBundles/angularbasic/src/main.ts +5 -0
- package/lib/templates/uiBundles/angularbasic/src/styles.css +12 -0
- package/lib/templates/uiBundles/angularbasic/src/types/sf-globals.d.ts +9 -0
- package/lib/templates/uiBundles/angularbasic/tsconfig.app.json +11 -0
- package/lib/templates/uiBundles/angularbasic/tsconfig.json +33 -0
- package/lib/templates/uiBundles/angularbasic/tsconfig.spec.json +10 -0
- package/lib/templates/uiBundles/angularbasic/ui-bundle.json +7 -0
- package/lib/templates/uiBundles/reactbasic/package-lock.json +784 -996
- package/lib/templates/uiBundles/reactbasic/package.json +4 -3
- package/lib/templates/uiBundles/reactbasic/scripts/get-graphql-schema.mjs +8 -5
- package/lib/templates/uiBundles/reactbasic/src/api/graphqlClient.ts +24 -8
- package/package.json +6 -5
|
@@ -22,15 +22,18 @@
|
|
|
22
22
|
* Permset assignment config (scripts/org-setup.config.json):
|
|
23
23
|
* {
|
|
24
24
|
* "permsetAssignments": {
|
|
25
|
+
* "defaultAssignee": "skip",
|
|
25
26
|
* "assignments": {
|
|
26
27
|
* "My_Permset": { "assignee": "currentUser" },
|
|
27
|
-
* "Guest_Permset": { "assignee": "guestUser"
|
|
28
|
+
* "Guest_Permset": { "assignee": "guestUser" },
|
|
28
29
|
* "Internal_Only": { "assignee": "skip" }
|
|
29
30
|
* }
|
|
30
31
|
* }
|
|
31
32
|
* }
|
|
32
|
-
* Assignee values: "currentUser", "
|
|
33
|
-
*
|
|
33
|
+
* Assignee values: "currentUser", "guestUser", or "skip". For "guestUser" the
|
|
34
|
+
* site is derived from the single networks/<siteName>.network-meta.xml the app
|
|
35
|
+
* ships — it is not restated per assignment.
|
|
36
|
+
* Unlisted permsets resolve to "defaultAssignee" (default "skip").
|
|
34
37
|
*/
|
|
35
38
|
|
|
36
39
|
import { spawnSync, spawn as nodeSpawn } from 'node:child_process';
|
|
@@ -38,9 +41,18 @@ import { resolve, dirname } from 'node:path';
|
|
|
38
41
|
import { fileURLToPath } from 'node:url';
|
|
39
42
|
import { readdirSync, existsSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';
|
|
40
43
|
|
|
44
|
+
import { validateConfig } from './org-setup-config-schema.mjs';
|
|
45
|
+
|
|
41
46
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
42
47
|
const ROOT = resolve(__dirname, '..');
|
|
43
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Thrown by step runners (run/runAsync) when a subprocess fails. The per-step
|
|
51
|
+
* orchestration in main() catches it, records the failure in the result ledger,
|
|
52
|
+
* and either aborts (fail-fast steps) or continues (skippable steps).
|
|
53
|
+
*/
|
|
54
|
+
class StepError extends Error {}
|
|
55
|
+
|
|
44
56
|
/**
|
|
45
57
|
* npm strips .gitignore from published packages — generate them on first run.
|
|
46
58
|
* Templates are stored in scripts/gitignore-templates.json (generated at build
|
|
@@ -149,15 +161,17 @@ Permset config (scripts/org-setup.config.json):
|
|
|
149
161
|
Control per-permset assignment via a config file. Example:
|
|
150
162
|
{
|
|
151
163
|
"permsetAssignments": {
|
|
164
|
+
"defaultAssignee": "skip",
|
|
152
165
|
"assignments": {
|
|
153
166
|
"My_Permset": { "assignee": "currentUser" },
|
|
154
|
-
"Guest_Permset": { "assignee": "guestUser"
|
|
167
|
+
"Guest_Permset": { "assignee": "guestUser" },
|
|
155
168
|
"Internal_Only": { "assignee": "skip" }
|
|
156
169
|
}
|
|
157
170
|
}
|
|
158
171
|
}
|
|
159
|
-
Assignee values: "currentUser", "
|
|
160
|
-
|
|
172
|
+
Assignee values: "currentUser", "guestUser", or "skip". For "guestUser" the site
|
|
173
|
+
is derived from the single networks/<siteName>.network-meta.xml the app ships.
|
|
174
|
+
Unlisted permsets resolve to "defaultAssignee" (default "skip").
|
|
161
175
|
`);
|
|
162
176
|
process.exit(0);
|
|
163
177
|
}
|
|
@@ -212,15 +226,64 @@ function discoverPermissionSetNames() {
|
|
|
212
226
|
return names.sort();
|
|
213
227
|
}
|
|
214
228
|
|
|
229
|
+
const CONFIG_PATH = resolve(__dirname, 'org-setup.config.json');
|
|
230
|
+
|
|
215
231
|
/**
|
|
216
|
-
*
|
|
232
|
+
* Read + validate org-setup.config.json ONCE, against the shared zod schema
|
|
233
|
+
* (the same `validateConfig` the build/CI gate uses). Exits non-zero with the
|
|
234
|
+
* precise zod issues if the config is invalid — before any step runs.
|
|
235
|
+
*
|
|
236
|
+
* Returns the validated config object (zod defaults applied), or an empty
|
|
237
|
+
* object when the file is absent (every section is optional).
|
|
238
|
+
*
|
|
239
|
+
* This is the single source of truth: loadPermsetConfig / loadRoleConfig /
|
|
240
|
+
* loadSelfRegConfig all read from the object it returns, so they no longer
|
|
241
|
+
* parse or defensively swallow errors.
|
|
242
|
+
*/
|
|
243
|
+
function loadValidatedConfig() {
|
|
244
|
+
if (!existsSync(CONFIG_PATH)) return {};
|
|
245
|
+
const result = validateConfig(readFileSync(CONFIG_PATH, 'utf8'), CONFIG_PATH);
|
|
246
|
+
if (!result.ok) {
|
|
247
|
+
console.error('Invalid org-setup.config.json:');
|
|
248
|
+
for (const err of result.errors) console.error(` - ${err}`);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
return result.data;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Derive the site name from the single networks/<siteName>.network-meta.xml the
|
|
256
|
+
* app ships. An app ships exactly one site, so the site name is derivable from
|
|
257
|
+
* deployed metadata rather than restated per-assignment (spec §5.2). Returns
|
|
258
|
+
* null when there is no networks dir or no .network-meta.xml file.
|
|
259
|
+
*/
|
|
260
|
+
function deriveSiteName() {
|
|
261
|
+
const networksDir = resolve(SFDX_SOURCE, 'networks');
|
|
262
|
+
if (!existsSync(networksDir)) return null;
|
|
263
|
+
const files = readdirSync(networksDir)
|
|
264
|
+
.filter((f) => f.endsWith('.network-meta.xml'))
|
|
265
|
+
.sort();
|
|
266
|
+
if (files.length === 0) return null;
|
|
267
|
+
// An app ships exactly one site; if a developer added a second, derivation is
|
|
268
|
+
// ambiguous — fail loudly rather than silently bind to an arbitrary site.
|
|
269
|
+
if (files.length > 1) {
|
|
270
|
+
throw new StepError(
|
|
271
|
+
`cannot derive guest site: multiple network metadata files found in ${networksDir} (${files.join(', ')}); guestUser assignment requires exactly one`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
return files[0].replace(/\.network-meta\.xml$/, '');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Permset assignment configuration, read from the already-validated config.
|
|
217
279
|
*
|
|
218
280
|
* Config shape:
|
|
219
281
|
* {
|
|
220
282
|
* "permsetAssignments": {
|
|
283
|
+
* "defaultAssignee": "skip",
|
|
221
284
|
* "assignments": {
|
|
222
285
|
* "My_Permset": { "assignee": "currentUser" },
|
|
223
|
-
* "My_Guest_Permset": { "assignee": "guestUser"
|
|
286
|
+
* "My_Guest_Permset": { "assignee": "guestUser" },
|
|
224
287
|
* "Internal_Only": { "assignee": "skip" }
|
|
225
288
|
* }
|
|
226
289
|
* }
|
|
@@ -229,68 +292,57 @@ function discoverPermissionSetNames() {
|
|
|
229
292
|
* Assignee values:
|
|
230
293
|
* "currentUser" — assign to the user running the script
|
|
231
294
|
* "skip" — do not assign this permset
|
|
232
|
-
* "guestUser" — resolve the site guest user automatically (
|
|
233
|
-
*
|
|
295
|
+
* "guestUser" — resolve the site guest user automatically (site derived from
|
|
296
|
+
* the single networks/<siteName>.network-meta.xml the app ships)
|
|
234
297
|
*
|
|
235
|
-
*
|
|
298
|
+
* Unlisted permsets resolve to `defaultAssignee` (default "skip").
|
|
236
299
|
*
|
|
237
|
-
* Returns { assignments: Record<string, { assignee: string
|
|
300
|
+
* Returns { defaultAssignee: string, assignments: Record<string, { assignee: string }> }
|
|
238
301
|
*/
|
|
239
|
-
function loadPermsetConfig() {
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (!section) return defaults;
|
|
247
|
-
return {
|
|
248
|
-
assignments: section.assignments || {},
|
|
249
|
-
};
|
|
250
|
-
} catch (err) {
|
|
251
|
-
console.warn(`Warning: failed to parse org-setup.config.json: ${err.message}; using defaults.`);
|
|
252
|
-
return defaults;
|
|
253
|
-
}
|
|
302
|
+
function loadPermsetConfig(config) {
|
|
303
|
+
const section = config.permsetAssignments;
|
|
304
|
+
if (!section) return { defaultAssignee: 'skip', assignments: {} };
|
|
305
|
+
return {
|
|
306
|
+
defaultAssignee: section.defaultAssignee,
|
|
307
|
+
assignments: section.assignments,
|
|
308
|
+
};
|
|
254
309
|
}
|
|
255
310
|
|
|
256
311
|
/** Resolve the effective assignment config for a given permset name. */
|
|
257
312
|
function resolveAssignment(permsetName, permsetConfig) {
|
|
258
313
|
const override = permsetConfig.assignments[permsetName];
|
|
259
|
-
if (!override) return { assignee:
|
|
260
|
-
return { assignee: override.assignee
|
|
314
|
+
if (!override) return { assignee: permsetConfig.defaultAssignee };
|
|
315
|
+
return { assignee: override.assignee };
|
|
261
316
|
}
|
|
262
317
|
|
|
263
318
|
/**
|
|
264
|
-
*
|
|
319
|
+
* Role assignment config, read from the already-validated config.
|
|
265
320
|
*
|
|
266
321
|
* Config shape:
|
|
267
322
|
* { "role": { "assignee": "currentUser", "roleName": "Admin" } }
|
|
268
323
|
*
|
|
269
324
|
* Returns null if no "role" section exists in config (the step is hidden).
|
|
270
325
|
*/
|
|
271
|
-
function loadRoleConfig() {
|
|
272
|
-
const
|
|
273
|
-
if (!
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
return {
|
|
279
|
-
assignee: section.assignee || 'currentUser',
|
|
280
|
-
roleName: section.roleName || null,
|
|
281
|
-
};
|
|
282
|
-
} catch {
|
|
283
|
-
return null;
|
|
284
|
-
}
|
|
326
|
+
function loadRoleConfig(config) {
|
|
327
|
+
const section = config.role;
|
|
328
|
+
if (!section) return null;
|
|
329
|
+
return {
|
|
330
|
+
assignee: section.assignee,
|
|
331
|
+
roleName: section.roleName,
|
|
332
|
+
};
|
|
285
333
|
}
|
|
286
334
|
|
|
287
335
|
/**
|
|
288
|
-
*
|
|
336
|
+
* Self-registration config, read from the already-validated config. The site is
|
|
337
|
+
* NOT stored here — it is derived from the single
|
|
338
|
+
* networks/<siteName>.network-meta.xml the app ships (spec §5.2), exactly like
|
|
339
|
+
* the guestUser permset path. deriveSiteName() is called lazily inside the
|
|
340
|
+
* selfReg step body so its "multiple network files" StepError is recorded in
|
|
341
|
+
* the ledger rather than escaping config load.
|
|
289
342
|
*
|
|
290
343
|
* Config shape:
|
|
291
344
|
* {
|
|
292
345
|
* "selfRegistration": {
|
|
293
|
-
* "siteName": "myapp",
|
|
294
346
|
* "selfRegProfile": "myapp Profile",
|
|
295
347
|
* "accountName": "My Self-Reg Account"
|
|
296
348
|
* }
|
|
@@ -298,21 +350,13 @@ function loadRoleConfig() {
|
|
|
298
350
|
*
|
|
299
351
|
* Returns null if no "selfRegistration" section exists in config (the step is hidden).
|
|
300
352
|
*/
|
|
301
|
-
function loadSelfRegConfig() {
|
|
302
|
-
const
|
|
303
|
-
if (!
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
return {
|
|
309
|
-
siteName: section.siteName || null,
|
|
310
|
-
selfRegProfile: section.selfRegProfile || null,
|
|
311
|
-
accountName: section.accountName || null,
|
|
312
|
-
};
|
|
313
|
-
} catch {
|
|
314
|
-
return null;
|
|
315
|
-
}
|
|
353
|
+
function loadSelfRegConfig(config) {
|
|
354
|
+
const section = config.selfRegistration;
|
|
355
|
+
if (!section) return null;
|
|
356
|
+
return {
|
|
357
|
+
selfRegProfile: section.selfRegProfile,
|
|
358
|
+
accountName: section.accountName,
|
|
359
|
+
};
|
|
316
360
|
}
|
|
317
361
|
|
|
318
362
|
/**
|
|
@@ -320,8 +364,8 @@ function loadSelfRegConfig() {
|
|
|
320
364
|
* This must happen BEFORE the initial deploy so that the profile is a recognised
|
|
321
365
|
* site member when subsequent steps (selfRegProfile, selfRegistration=true) are deployed.
|
|
322
366
|
*/
|
|
323
|
-
function ensureNetworkMemberProfile(selfRegConfig) {
|
|
324
|
-
const {
|
|
367
|
+
function ensureNetworkMemberProfile(selfRegConfig, siteName) {
|
|
368
|
+
const { selfRegProfile } = selfRegConfig;
|
|
325
369
|
if (!siteName || !selfRegProfile) return;
|
|
326
370
|
|
|
327
371
|
const networkXmlPath = resolve(SFDX_SOURCE, 'networks', `${siteName}.network-meta.xml`);
|
|
@@ -356,14 +400,13 @@ function ensureNetworkMemberProfile(selfRegConfig) {
|
|
|
356
400
|
* 3. Create an Account record (idempotent).
|
|
357
401
|
* 4. Create a NetworkSelfRegistration record linking the Account to the Network (idempotent).
|
|
358
402
|
*/
|
|
359
|
-
function enableSelfRegistration(selfRegConfig, targetOrg) {
|
|
360
|
-
const {
|
|
403
|
+
function enableSelfRegistration(selfRegConfig, siteName, targetOrg) {
|
|
404
|
+
const { selfRegProfile, accountName } = selfRegConfig;
|
|
361
405
|
|
|
362
406
|
// 1. Modify network metadata XML
|
|
363
407
|
const networkXmlPath = resolve(SFDX_SOURCE, 'networks', `${siteName}.network-meta.xml`);
|
|
364
408
|
if (!existsSync(networkXmlPath)) {
|
|
365
|
-
|
|
366
|
-
return;
|
|
409
|
+
throw new StepError(`network metadata not found: ${networkXmlPath}`);
|
|
367
410
|
}
|
|
368
411
|
const xml = readFileSync(networkXmlPath, 'utf8');
|
|
369
412
|
|
|
@@ -393,8 +436,7 @@ function enableSelfRegistration(selfRegConfig, targetOrg) {
|
|
|
393
436
|
'--source-dir', networkXmlPath,
|
|
394
437
|
], { cwd: ROOT, stdio: 'inherit', shell: true, timeout: 120000 });
|
|
395
438
|
if (deployResult.status !== 0) {
|
|
396
|
-
|
|
397
|
-
process.exit(deployResult.status ?? 1);
|
|
439
|
+
throw new StepError(`failed to deploy updated network metadata (exit ${deployResult.status ?? 1})`);
|
|
398
440
|
}
|
|
399
441
|
}
|
|
400
442
|
|
|
@@ -424,17 +466,15 @@ function enableSelfRegistration(selfRegConfig, targetOrg) {
|
|
|
424
466
|
'--json',
|
|
425
467
|
], { cwd: ROOT, encoding: 'utf8' });
|
|
426
468
|
if (createResult.status !== 0) {
|
|
427
|
-
console.error(` Failed to create Account "${accountName}".`);
|
|
428
469
|
if (createResult.stderr) console.error(createResult.stderr);
|
|
429
|
-
|
|
470
|
+
throw new StepError(`failed to create Account "${accountName}"`);
|
|
430
471
|
}
|
|
431
472
|
try {
|
|
432
473
|
const json = JSON.parse(createResult.stdout);
|
|
433
474
|
accountId = json.result?.id;
|
|
434
475
|
console.log(` Created Account "${accountName}" (${accountId}).`);
|
|
435
476
|
} catch {
|
|
436
|
-
|
|
437
|
-
return;
|
|
477
|
+
throw new StepError('failed to parse Account creation result');
|
|
438
478
|
}
|
|
439
479
|
}
|
|
440
480
|
|
|
@@ -454,8 +494,7 @@ function enableSelfRegistration(selfRegConfig, targetOrg) {
|
|
|
454
494
|
} catch { /* fall through */ }
|
|
455
495
|
}
|
|
456
496
|
if (!networkId) {
|
|
457
|
-
|
|
458
|
-
return;
|
|
497
|
+
throw new StepError(`could not find Network "${siteName}" in org`);
|
|
459
498
|
}
|
|
460
499
|
console.log(` Found Network "${siteName}" (${networkId}).`);
|
|
461
500
|
|
|
@@ -493,9 +532,8 @@ function enableSelfRegistration(selfRegConfig, targetOrg) {
|
|
|
493
532
|
const apexOut = apexResult.stdout?.toString() || '';
|
|
494
533
|
if (existsSync(tmpApex)) unlinkSync(tmpApex);
|
|
495
534
|
if (apexResult.status !== 0 && !apexOut.includes('Compiled successfully')) {
|
|
496
|
-
console.error(' Failed to create NetworkSelfRegistration record.');
|
|
497
535
|
process.stderr.write(apexResult.stderr?.toString() || apexOut);
|
|
498
|
-
|
|
536
|
+
throw new StepError('failed to create NetworkSelfRegistration record');
|
|
499
537
|
}
|
|
500
538
|
const nsrMatch = apexOut.match(/NSR_CREATED:(\w+)/);
|
|
501
539
|
if (nsrMatch) {
|
|
@@ -519,22 +557,20 @@ function assignRoleToCurrentUser(roleName, targetOrg) {
|
|
|
519
557
|
'--json',
|
|
520
558
|
], { cwd: ROOT, encoding: 'utf8' });
|
|
521
559
|
if (roleResult.status !== 0) {
|
|
522
|
-
console.error(` Failed to query role "${roleName}" in org.`);
|
|
523
560
|
if (roleResult.stderr) console.error(roleResult.stderr);
|
|
524
|
-
|
|
561
|
+
throw new StepError(`failed to query role "${roleName}" in org`);
|
|
525
562
|
}
|
|
526
563
|
let roleId;
|
|
527
564
|
try {
|
|
528
565
|
const json = JSON.parse(roleResult.stdout);
|
|
529
566
|
const records = json.result?.records;
|
|
530
567
|
if (!records || records.length === 0) {
|
|
531
|
-
|
|
532
|
-
return;
|
|
568
|
+
throw new StepError(`role "${roleName}" not found in org`);
|
|
533
569
|
}
|
|
534
570
|
roleId = records[0].Id;
|
|
535
|
-
} catch {
|
|
536
|
-
|
|
537
|
-
|
|
571
|
+
} catch (err) {
|
|
572
|
+
if (err instanceof StepError) throw err;
|
|
573
|
+
throw new StepError(`failed to parse role query result for "${roleName}"`);
|
|
538
574
|
}
|
|
539
575
|
|
|
540
576
|
const orgResult = spawnSync('sf', [
|
|
@@ -543,20 +579,18 @@ function assignRoleToCurrentUser(roleName, targetOrg) {
|
|
|
543
579
|
'--json',
|
|
544
580
|
], { cwd: ROOT, encoding: 'utf8' });
|
|
545
581
|
if (orgResult.status !== 0) {
|
|
546
|
-
|
|
547
|
-
return;
|
|
582
|
+
throw new StepError('failed to resolve current user from org');
|
|
548
583
|
}
|
|
549
584
|
let username;
|
|
550
585
|
try {
|
|
551
586
|
const json = JSON.parse(orgResult.stdout);
|
|
552
587
|
username = json.result?.username;
|
|
553
588
|
if (!username) {
|
|
554
|
-
|
|
555
|
-
return;
|
|
589
|
+
throw new StepError('could not determine current username from org display');
|
|
556
590
|
}
|
|
557
|
-
} catch {
|
|
558
|
-
|
|
559
|
-
|
|
591
|
+
} catch (err) {
|
|
592
|
+
if (err instanceof StepError) throw err;
|
|
593
|
+
throw new StepError('failed to parse org display result');
|
|
560
594
|
}
|
|
561
595
|
|
|
562
596
|
const userQuery = `SELECT Id, UserRoleId FROM User WHERE Username = '${username}'`;
|
|
@@ -589,8 +623,8 @@ function assignRoleToCurrentUser(roleName, targetOrg) {
|
|
|
589
623
|
console.log(` Role "${roleName}" assigned to ${username}.`);
|
|
590
624
|
} else {
|
|
591
625
|
const out = (updateResult.stderr?.toString() || '') + (updateResult.stdout?.toString() || '');
|
|
592
|
-
console.error(` Failed to assign role "${roleName}" to ${username}.`);
|
|
593
626
|
if (out) console.error(out);
|
|
627
|
+
throw new StepError(`failed to assign role "${roleName}" to ${username}`);
|
|
594
628
|
}
|
|
595
629
|
}
|
|
596
630
|
|
|
@@ -677,9 +711,13 @@ function buildApexInsert(sobject, records, refIds) {
|
|
|
677
711
|
async function promptSteps(steps) {
|
|
678
712
|
if (!process.stdin.isTTY) return steps.map((s) => s.enabled);
|
|
679
713
|
|
|
714
|
+
// `selected` stays indexed by ORIGINAL step order (so the caller's
|
|
715
|
+
// selections[i] → stepDefs[i] mapping is unchanged); unavailable steps remain
|
|
716
|
+
// false. Only available steps are shown and navigable — unavailable steps are
|
|
717
|
+
// hidden entirely rather than rendered greyed-out.
|
|
680
718
|
const selected = steps.map((s) => s.enabled);
|
|
719
|
+
const visible = steps.map((s, i) => ({ step: s, index: i })).filter(({ step }) => step.available);
|
|
681
720
|
let cursor = 0;
|
|
682
|
-
const DIM = '\x1B[2m';
|
|
683
721
|
const RST = '\x1B[0m';
|
|
684
722
|
const CYAN = '\x1B[36m';
|
|
685
723
|
const GREEN = '\x1B[32m';
|
|
@@ -701,11 +739,10 @@ async function promptSteps(steps) {
|
|
|
701
739
|
}
|
|
702
740
|
|
|
703
741
|
function render() {
|
|
704
|
-
return
|
|
705
|
-
const ptr =
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
return `${ptr} ${chk} ${s.label}`;
|
|
742
|
+
return visible.map(({ step, index }, row) => {
|
|
743
|
+
const ptr = row === cursor ? `${CYAN}❯${RST}` : ' ';
|
|
744
|
+
const chk = selected[index] ? `${GREEN}●${RST}` : '○';
|
|
745
|
+
return `${ptr} ${chk} ${step.label}`;
|
|
709
746
|
});
|
|
710
747
|
}
|
|
711
748
|
|
|
@@ -742,16 +779,17 @@ async function promptSteps(steps) {
|
|
|
742
779
|
resolve(selected);
|
|
743
780
|
return;
|
|
744
781
|
}
|
|
782
|
+
// Note: `cursor` indexes `visible`; `selected` is indexed by ORIGINAL step
|
|
783
|
+
// order. Map through visible[cursor].index before touching `selected`.
|
|
745
784
|
if (key === ' ') {
|
|
746
|
-
|
|
785
|
+
const { index } = visible[cursor];
|
|
786
|
+
selected[index] = !selected[index];
|
|
747
787
|
redraw();
|
|
748
788
|
return;
|
|
749
789
|
}
|
|
750
790
|
if (key === 'a') {
|
|
751
|
-
const allOn =
|
|
752
|
-
for (
|
|
753
|
-
if (steps[i].available) selected[i] = !allOn;
|
|
754
|
-
}
|
|
791
|
+
const allOn = visible.every(({ index }) => selected[index]);
|
|
792
|
+
for (const { index } of visible) selected[index] = !allOn;
|
|
755
793
|
redraw();
|
|
756
794
|
return;
|
|
757
795
|
}
|
|
@@ -759,7 +797,7 @@ async function promptSteps(steps) {
|
|
|
759
797
|
cursor = Math.max(0, cursor - 1);
|
|
760
798
|
redraw();
|
|
761
799
|
} else if (key === '\x1B[B' || key === 'j') {
|
|
762
|
-
cursor = Math.min(
|
|
800
|
+
cursor = Math.min(visible.length - 1, cursor + 1);
|
|
763
801
|
redraw();
|
|
764
802
|
}
|
|
765
803
|
});
|
|
@@ -773,11 +811,11 @@ function run(name, cmd, args, opts = {}) {
|
|
|
773
811
|
cwd,
|
|
774
812
|
stdio: 'inherit',
|
|
775
813
|
shell: true,
|
|
814
|
+
...(opts.env && { env: opts.env }),
|
|
776
815
|
...(opts.timeout && { timeout: opts.timeout }),
|
|
777
816
|
});
|
|
778
817
|
if (result.status !== 0 && !optional) {
|
|
779
|
-
|
|
780
|
-
process.exit(result.status ?? 1);
|
|
818
|
+
throw new StepError(`${name} (exit ${result.status ?? 1})`);
|
|
781
819
|
}
|
|
782
820
|
return result;
|
|
783
821
|
}
|
|
@@ -807,12 +845,91 @@ async function runAsync(name, cmd, args, opts = {}) {
|
|
|
807
845
|
if (result.status !== 0 && !optional) {
|
|
808
846
|
if (result.stdout) process.stdout.write(result.stdout);
|
|
809
847
|
if (result.stderr) process.stderr.write(result.stderr);
|
|
810
|
-
|
|
811
|
-
process.exit(result.status ?? 1);
|
|
848
|
+
throw new StepError(`${name} (exit ${result.status ?? 1})`);
|
|
812
849
|
}
|
|
813
850
|
return result;
|
|
814
851
|
}
|
|
815
852
|
|
|
853
|
+
/**
|
|
854
|
+
* In-memory result ledger. Each selected step records exactly one outcome:
|
|
855
|
+
* ok — the step ran and succeeded
|
|
856
|
+
* skipped — the step was not selected, or had no config section (intentional)
|
|
857
|
+
* failed — the step ran and failed (with a human-readable reason)
|
|
858
|
+
* The end-of-run summary is rendered from this ledger and the process exits
|
|
859
|
+
* non-zero whenever any step is `failed` (fail-fast or skippable alike).
|
|
860
|
+
*
|
|
861
|
+
* @typedef {{ key: string, label: string, status: 'ok'|'skipped'|'failed', reason?: string, failFast?: boolean }} StepResult
|
|
862
|
+
*/
|
|
863
|
+
const results = [];
|
|
864
|
+
function recordOk(step) {
|
|
865
|
+
results.push({ key: step.key, label: step.label, status: 'ok', failFast: step.failFast });
|
|
866
|
+
}
|
|
867
|
+
function recordSkipped(step, reason) {
|
|
868
|
+
results.push({ key: step.key, label: step.label, status: 'skipped', reason, failFast: step.failFast });
|
|
869
|
+
}
|
|
870
|
+
function recordFailed(step, reason) {
|
|
871
|
+
results.push({ key: step.key, label: step.label, status: 'failed', reason, failFast: step.failFast });
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/** True if any step in the ledger failed. */
|
|
875
|
+
function finalExitCode() {
|
|
876
|
+
return results.some((r) => r.status === 'failed') ? 1 : 0;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const SUMMARY_GLYPH = { ok: '✔', skipped: '–', failed: '✖' };
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Render the end-of-run summary. Always called — on success, on a fail-fast
|
|
883
|
+
* abort (partial: only steps reached so far are present), and on a clean finish
|
|
884
|
+
* that had skippable failures. Failed rows are listed last so the real problem
|
|
885
|
+
* is the last thing the developer sees.
|
|
886
|
+
*/
|
|
887
|
+
function printSummary(targetOrg) {
|
|
888
|
+
const ordered = [
|
|
889
|
+
...results.filter((r) => r.status !== 'failed'),
|
|
890
|
+
...results.filter((r) => r.status === 'failed'),
|
|
891
|
+
];
|
|
892
|
+
const pad = Math.max(0, ...ordered.map((r) => r.key.length));
|
|
893
|
+
console.log(`\nSetup summary (target org: ${targetOrg})`);
|
|
894
|
+
for (const r of ordered) {
|
|
895
|
+
const glyph = SUMMARY_GLYPH[r.status] ?? ' ';
|
|
896
|
+
const key = r.key.padEnd(pad);
|
|
897
|
+
let line = ` ${glyph} ${key} ${r.status}`;
|
|
898
|
+
if (r.reason) line += ` (${r.reason})`;
|
|
899
|
+
if (r.status === 'failed') line += r.failFast ? ' [fail-fast — aborted]' : ' [skippable — continued]';
|
|
900
|
+
console.log(line);
|
|
901
|
+
}
|
|
902
|
+
const failures = results.filter((r) => r.status === 'failed').length;
|
|
903
|
+
if (failures > 0) {
|
|
904
|
+
console.log(`Setup completed with ${failures} failure(s). Exiting 1.`);
|
|
905
|
+
} else {
|
|
906
|
+
console.log('Setup complete.');
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Run one step body, recording its outcome in the ledger and honoring the
|
|
912
|
+
* step's fixed `failFast` classification. The body either completes (→ ok),
|
|
913
|
+
* throws a StepError (→ failed), or throws something unexpected (→ failed,
|
|
914
|
+
* with the raw message). On a fail-fast failure this prints the partial
|
|
915
|
+
* summary and exits non-zero immediately; on a skippable failure it records
|
|
916
|
+
* and returns so the run continues.
|
|
917
|
+
*/
|
|
918
|
+
async function runStep(step, targetOrg, body) {
|
|
919
|
+
try {
|
|
920
|
+
await body();
|
|
921
|
+
recordOk(step);
|
|
922
|
+
} catch (err) {
|
|
923
|
+
const reason = err instanceof StepError ? err.message : (err?.message ?? String(err));
|
|
924
|
+
recordFailed(step, reason);
|
|
925
|
+
console.error(`\nStep "${step.key}" failed: ${reason}`);
|
|
926
|
+
if (step.failFast) {
|
|
927
|
+
printSummary(targetOrg);
|
|
928
|
+
process.exit(1);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
816
933
|
async function main() {
|
|
817
934
|
// Ensure .gitignore files exist (npm strips them from published packages).
|
|
818
935
|
const gitignoreTemplates = loadGitignoreTemplates();
|
|
@@ -852,22 +969,30 @@ async function main() {
|
|
|
852
969
|
? `Permset — assign ${permsetNames.join(', ')}`
|
|
853
970
|
: `Permset — assign ${permsetNames.length} permission sets`;
|
|
854
971
|
|
|
972
|
+
// Validate org-setup.config.json ONCE, before any step runs (spec §5.2). The
|
|
973
|
+
// three load*Config helpers read from this already-validated object.
|
|
974
|
+
const config = loadValidatedConfig();
|
|
975
|
+
|
|
855
976
|
const hasDataPlan = existsSync(DATA_PLAN) && existsSync(DATA_DIR);
|
|
856
|
-
const roleConfig = loadRoleConfig();
|
|
977
|
+
const roleConfig = loadRoleConfig(config);
|
|
857
978
|
const hasRoleConfig = roleConfig !== null;
|
|
858
|
-
const selfRegConfig = loadSelfRegConfig();
|
|
979
|
+
const selfRegConfig = loadSelfRegConfig(config);
|
|
859
980
|
const hasSelfRegConfig = selfRegConfig !== null;
|
|
860
981
|
|
|
982
|
+
// failFast is a fixed, implementer-owned classification (spec §5.2) — NOT
|
|
983
|
+
// user-configurable. A fail-fast failure aborts the run immediately; a
|
|
984
|
+
// skippable failure is recorded and the run continues. The exit code is
|
|
985
|
+
// non-zero on any failure regardless of class.
|
|
861
986
|
const stepDefs = [
|
|
862
|
-
{ key: 'login', label: 'Login — org authentication', enabled: !argSkipLogin, available: true },
|
|
863
|
-
{ key: 'uiBundleBuild', label: 'UI Bundle Build — npm install + build (pre-deploy)', enabled: !argSkipUIBundleBuild, available: true },
|
|
864
|
-
{ key: 'deploy', label: 'Deploy — sf project deploy start', enabled: !argSkipDeploy, available: true },
|
|
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:
|
|
868
|
-
{ key: 'data', label: 'Data — delete + import records via Apex', enabled: !argSkipData && hasDataPlan, available: hasDataPlan },
|
|
869
|
-
{ key: 'graphql', label: 'GraphQL — schema introspect + codegen', enabled: !argSkipGraphql, available: true },
|
|
870
|
-
{ key: 'dev', label: 'Dev — launch dev server', enabled: !argSkipDev, available: true },
|
|
987
|
+
{ key: 'login', label: 'Login — org authentication', enabled: !argSkipLogin, available: true, failFast: true },
|
|
988
|
+
{ key: 'uiBundleBuild', label: 'UI Bundle Build — npm install + build (pre-deploy)', enabled: !argSkipUIBundleBuild, available: true, failFast: true },
|
|
989
|
+
{ key: 'deploy', label: 'Deploy — sf project deploy start', enabled: !argSkipDeploy, available: true, failFast: true },
|
|
990
|
+
{ key: 'permset', label: permsetStepLabel, enabled: !argSkipPermset, available: true, failFast: false },
|
|
991
|
+
{ key: 'role', label: `Role — assign "${roleConfig?.roleName ?? '?'}" to current user`, enabled: !argSkipRole && hasRoleConfig, available: hasRoleConfig, failFast: false },
|
|
992
|
+
{ key: 'selfReg', label: 'Self-Registration — enable for site', enabled: !argSkipSelfReg && hasSelfRegConfig, available: hasSelfRegConfig, failFast: false },
|
|
993
|
+
{ key: 'data', label: 'Data — delete + import records via Apex', enabled: !argSkipData && hasDataPlan, available: hasDataPlan, failFast: true },
|
|
994
|
+
{ key: 'graphql', label: 'GraphQL — schema introspect + codegen', enabled: !argSkipGraphql, available: true, failFast: true },
|
|
995
|
+
{ key: 'dev', label: 'Dev — launch dev server', enabled: !argSkipDev, available: true, failFast: false },
|
|
871
996
|
];
|
|
872
997
|
|
|
873
998
|
const selections = yes ? stepDefs.map((s) => s.enabled) : await promptSteps(stepDefs);
|
|
@@ -904,48 +1029,94 @@ async function main() {
|
|
|
904
1029
|
!skipDev
|
|
905
1030
|
);
|
|
906
1031
|
|
|
1032
|
+
const loginStep = stepDefs.find((s) => s.key === 'login');
|
|
907
1033
|
if (!skipLogin) {
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1034
|
+
await runStep(loginStep, targetOrg, async () => {
|
|
1035
|
+
if (isOrgConnected(targetOrg)) {
|
|
1036
|
+
console.log('\n--- Login ---');
|
|
1037
|
+
console.log(`Org ${targetOrg} is already authenticated; skipping browser login.`);
|
|
1038
|
+
} else {
|
|
1039
|
+
// Login is fail-fast (spec §5.2): drop the previous { optional: true } so a
|
|
1040
|
+
// failed browser login records `failed`, prints the summary, and aborts.
|
|
1041
|
+
run('Login (browser)', 'sf', ['org', 'login', 'web', '--alias', targetOrg]);
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
} else {
|
|
1045
|
+
recordSkipped(loginStep, 'not selected');
|
|
914
1046
|
}
|
|
915
1047
|
|
|
916
1048
|
// Ensure the self-reg profile is in networkMemberGroups before deploy so that
|
|
917
|
-
// subsequent selfRegProfile / selfRegistration updates don't fail.
|
|
1049
|
+
// subsequent selfRegProfile / selfRegistration updates don't fail. This is
|
|
1050
|
+
// best-effort prep: if the site can't be derived (no network file, or the
|
|
1051
|
+
// ambiguous multi-network case where deriveSiteName throws), skip the prep
|
|
1052
|
+
// silently here — the selfReg step records that derivation failure as a
|
|
1053
|
+
// skippable StepError, so it stays in the ledger instead of aborting pre-deploy.
|
|
918
1054
|
if (!skipDeploy && selfRegConfig) {
|
|
919
|
-
|
|
920
|
-
|
|
1055
|
+
let preDeploySiteName = null;
|
|
1056
|
+
try {
|
|
1057
|
+
preDeploySiteName = deriveSiteName();
|
|
1058
|
+
} catch {
|
|
1059
|
+
// ambiguous derivation — surfaced by the selfReg step below
|
|
1060
|
+
}
|
|
1061
|
+
if (preDeploySiteName) {
|
|
1062
|
+
console.log('\n--- Ensure network member profile (pre-deploy) ---');
|
|
1063
|
+
ensureNetworkMemberProfile(selfRegConfig, preDeploySiteName);
|
|
1064
|
+
}
|
|
921
1065
|
}
|
|
922
1066
|
|
|
923
1067
|
// Build all UI Bundles before deploy so dist exists for entity deployment
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1068
|
+
const uiBundleBuildStep = stepDefs.find((s) => s.key === 'uiBundleBuild');
|
|
1069
|
+
let preDeployBundlesBuilt = false;
|
|
1070
|
+
if (!skipUIBundleBuild) {
|
|
1071
|
+
if (!skipDeploy) {
|
|
1072
|
+
await runStep(uiBundleBuildStep, targetOrg, () => {
|
|
1073
|
+
const allUIBundleDirs = discoverAllUIBundleDirs(uiBundleName);
|
|
1074
|
+
for (const dir of allUIBundleDirs) {
|
|
1075
|
+
const name = dir.split(/[/\\]/).pop();
|
|
1076
|
+
run(`UI Bundle install (${name})`, 'npm', ['install'], { cwd: dir });
|
|
1077
|
+
run(`UI Bundle build (${name})`, 'npm', ['run', 'build'], { cwd: dir });
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
preDeployBundlesBuilt = true;
|
|
930
1081
|
}
|
|
1082
|
+
// When skipDeploy, the bundle build happens in the GraphQL section below;
|
|
1083
|
+
// its outcome is recorded there.
|
|
1084
|
+
} else {
|
|
1085
|
+
recordSkipped(uiBundleBuildStep, 'not selected');
|
|
931
1086
|
}
|
|
932
1087
|
|
|
1088
|
+
const deployStep = stepDefs.find((s) => s.key === 'deploy');
|
|
933
1089
|
if (!skipDeploy) {
|
|
934
|
-
|
|
935
|
-
|
|
1090
|
+
await runStep(deployStep, targetOrg, () => {
|
|
1091
|
+
run('Deploy metadata', 'sf', ['project', 'deploy', 'start', '--target-org', targetOrg], {
|
|
1092
|
+
timeout: 180000,
|
|
1093
|
+
});
|
|
936
1094
|
});
|
|
1095
|
+
} else {
|
|
1096
|
+
recordSkipped(deployStep, 'not selected');
|
|
937
1097
|
}
|
|
938
1098
|
|
|
1099
|
+
const permsetStep = stepDefs.find((s) => s.key === 'permset');
|
|
939
1100
|
if (!skipPermset) {
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1101
|
+
await runStep(permsetStep, targetOrg, async () => {
|
|
1102
|
+
const permsetConfig = loadPermsetConfig(config);
|
|
1103
|
+
if (permsetNames.length === 0) {
|
|
1104
|
+
console.log('\n--- Assign permission sets ---');
|
|
1105
|
+
console.log('No permission sets found under permissionsets/ and none passed via --permset-name; skipping.');
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
945
1108
|
console.log('\n--- Assign permission sets ---');
|
|
946
1109
|
|
|
947
1110
|
// Resolve assignments (guest user lookups etc.) then run all sf assign calls in parallel.
|
|
1111
|
+
//
|
|
1112
|
+
// A guest-user resolution failure (no derivable site, or no guest user yet)
|
|
1113
|
+
// is collected — NOT thrown mid-loop. Throwing here would unwind the whole
|
|
1114
|
+
// runStep body before Promise.all and silently drop every assignment already
|
|
1115
|
+
// queued (e.g. a currentUser permset that sorts ahead of a guestUser one and
|
|
1116
|
+
// never depended on the site at all). Resolvable assignments still run; the
|
|
1117
|
+
// combined failure is thrown at the end so the step is still recorded failed.
|
|
948
1118
|
const assignmentJobs = [];
|
|
1119
|
+
const resolutionFailures = [];
|
|
949
1120
|
for (const permsetName of permsetNames) {
|
|
950
1121
|
const assignment = resolveAssignment(permsetName, permsetConfig);
|
|
951
1122
|
if (assignment.assignee === 'skip') {
|
|
@@ -954,18 +1125,21 @@ async function main() {
|
|
|
954
1125
|
}
|
|
955
1126
|
let effectiveUsername = null;
|
|
956
1127
|
if (assignment.assignee === 'guestUser') {
|
|
957
|
-
|
|
958
|
-
|
|
1128
|
+
// Site name is derived from the single network metadata file the app
|
|
1129
|
+
// ships (spec §5.2) — never restated per-assignment.
|
|
1130
|
+
const siteName = deriveSiteName();
|
|
1131
|
+
if (!siteName) {
|
|
1132
|
+
console.error(`Permission set "${permsetName}" — assignee is "guestUser" but no networks/<siteName>.network-meta.xml was found to derive the site; skipping.`);
|
|
1133
|
+
resolutionFailures.push(`${permsetName} (no network metadata to derive site)`);
|
|
959
1134
|
continue;
|
|
960
1135
|
}
|
|
961
|
-
effectiveUsername = resolveGuestUsername(
|
|
1136
|
+
effectiveUsername = resolveGuestUsername(siteName, targetOrg);
|
|
962
1137
|
if (!effectiveUsername) {
|
|
963
|
-
console.error(`Permission set "${permsetName}" — could not resolve guest user for site "${
|
|
1138
|
+
console.error(`Permission set "${permsetName}" — could not resolve guest user for site "${siteName}"; skipping.`);
|
|
1139
|
+
resolutionFailures.push(`${permsetName} (could not resolve guest user for site "${siteName}")`);
|
|
964
1140
|
continue;
|
|
965
1141
|
}
|
|
966
|
-
console.log(` Resolved guest user for site "${
|
|
967
|
-
} else if (assignment.assignee !== 'currentUser') {
|
|
968
|
-
effectiveUsername = assignment.assignee;
|
|
1142
|
+
console.log(` Resolved guest user for site "${siteName}": ${effectiveUsername}`);
|
|
969
1143
|
}
|
|
970
1144
|
assignmentJobs.push({ permsetName, effectiveUsername });
|
|
971
1145
|
}
|
|
@@ -981,6 +1155,7 @@ async function main() {
|
|
|
981
1155
|
return { permsetName, assigneeLabel, result };
|
|
982
1156
|
}));
|
|
983
1157
|
|
|
1158
|
+
const failures = [];
|
|
984
1159
|
for (const { permsetName, assigneeLabel, result } of assignResults) {
|
|
985
1160
|
if (result.status === 0) {
|
|
986
1161
|
console.log(`Permission set "${permsetName}" assigned to ${assigneeLabel}.`);
|
|
@@ -993,35 +1168,55 @@ async function main() {
|
|
|
993
1168
|
} else {
|
|
994
1169
|
if (result.stdout) process.stdout.write(result.stdout);
|
|
995
1170
|
if (result.stderr) process.stderr.write(result.stderr);
|
|
996
|
-
|
|
997
|
-
process.exit(result.status ?? 1);
|
|
1171
|
+
failures.push(`${permsetName} (exit ${result.status ?? 1})`);
|
|
998
1172
|
}
|
|
999
1173
|
}
|
|
1000
1174
|
}
|
|
1001
|
-
|
|
1175
|
+
const allFailures = [...resolutionFailures, ...failures];
|
|
1176
|
+
if (allFailures.length > 0) {
|
|
1177
|
+
throw new StepError(`failed to assign permission set(s): ${allFailures.join(', ')}`);
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
} else {
|
|
1181
|
+
recordSkipped(permsetStep, 'not selected');
|
|
1002
1182
|
}
|
|
1003
1183
|
|
|
1184
|
+
const roleStep = stepDefs.find((s) => s.key === 'role');
|
|
1004
1185
|
if (!skipRole) {
|
|
1005
1186
|
console.log('\n--- Assign role ---');
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
} else {
|
|
1187
|
+
// Config shape (assignee === 'currentUser', non-empty roleName) is guaranteed
|
|
1188
|
+
// by the schema, so there is nothing to re-check here — the step either ran
|
|
1189
|
+
// (ok) or its assignment threw (failed). No manual recordSkipped inference.
|
|
1190
|
+
await runStep(roleStep, targetOrg, () => {
|
|
1011
1191
|
assignRoleToCurrentUser(roleConfig.roleName, targetOrg);
|
|
1012
|
-
}
|
|
1192
|
+
});
|
|
1193
|
+
} else {
|
|
1194
|
+
recordSkipped(roleStep, roleStep.available ? 'not selected' : 'no config');
|
|
1013
1195
|
}
|
|
1014
1196
|
|
|
1197
|
+
const selfRegStep = stepDefs.find((s) => s.key === 'selfReg');
|
|
1015
1198
|
if (!skipSelfReg) {
|
|
1016
1199
|
console.log('\n--- Enable self-registration ---');
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1200
|
+
// Config shape (selfRegProfile, accountName) is guaranteed by the schema.
|
|
1201
|
+
// The site is derived inside the step body so a "multiple network files"
|
|
1202
|
+
// StepError is recorded in the ledger rather than escaping. No manual
|
|
1203
|
+
// recordSkipped inference.
|
|
1204
|
+
await runStep(selfRegStep, targetOrg, () => {
|
|
1205
|
+
const siteName = deriveSiteName();
|
|
1206
|
+
if (!siteName) {
|
|
1207
|
+
throw new StepError(
|
|
1208
|
+
'self-registration is configured but no networks/<siteName>.network-meta.xml was found to derive the site',
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
enableSelfRegistration(selfRegConfig, siteName, targetOrg);
|
|
1212
|
+
});
|
|
1213
|
+
} else {
|
|
1214
|
+
recordSkipped(selfRegStep, selfRegStep.available ? 'not selected' : 'no config');
|
|
1022
1215
|
}
|
|
1023
1216
|
|
|
1217
|
+
const dataStep = stepDefs.find((s) => s.key === 'data');
|
|
1024
1218
|
if (doData) {
|
|
1219
|
+
await runStep(dataStep, targetOrg, () => {
|
|
1025
1220
|
// Prepare data for uniqueness (run before import so repeat imports don't conflict)
|
|
1026
1221
|
const prepareScript = resolve(__dirname, 'prepare-import-unique-fields.js');
|
|
1027
1222
|
run('Prepare data (unique fields)', 'node', [prepareScript, '--data-dir', DATA_DIR], {
|
|
@@ -1113,9 +1308,8 @@ async function main() {
|
|
|
1113
1308
|
const apexOut = apexResult.stdout?.toString() || '';
|
|
1114
1309
|
const apexErr = apexResult.stderr?.toString() || '';
|
|
1115
1310
|
if (apexResult.status !== 0 && !apexOut.includes('Compiled successfully')) {
|
|
1116
|
-
console.error(` ${entry.sobject}: apex execution failed`);
|
|
1117
1311
|
process.stderr.write(apexErr || apexOut);
|
|
1118
|
-
|
|
1312
|
+
throw new StepError(`${entry.sobject}: apex execution failed`);
|
|
1119
1313
|
}
|
|
1120
1314
|
const okMatches = [...apexOut.matchAll(/\|DEBUG\|REF:([^:\n]+):(\w+)/g)];
|
|
1121
1315
|
const errMatches = [...apexOut.matchAll(/\|DEBUG\|ERR:([^:\n]+):([^\n]+)/g)];
|
|
@@ -1124,8 +1318,7 @@ async function main() {
|
|
|
1124
1318
|
console.error(` ${m[1]}: ${m[2].trim()}`);
|
|
1125
1319
|
}
|
|
1126
1320
|
if (errMatches.length > 5) console.error(` ... and ${errMatches.length - 5} more`);
|
|
1127
|
-
|
|
1128
|
-
process.exit(1);
|
|
1321
|
+
throw new StepError(`data import tree (${entry.sobject}) — ${errMatches.length} record error(s)`);
|
|
1129
1322
|
}
|
|
1130
1323
|
if (entry.saveRefs) {
|
|
1131
1324
|
for (const m of okMatches) refMap.set(m[1], m[2]);
|
|
@@ -1136,26 +1329,66 @@ async function main() {
|
|
|
1136
1329
|
}
|
|
1137
1330
|
}
|
|
1138
1331
|
if (existsSync(tmpApex)) unlinkSync(tmpApex);
|
|
1332
|
+
});
|
|
1333
|
+
} else {
|
|
1334
|
+
recordSkipped(dataStep, dataStep.available ? 'not selected' : 'no data plan');
|
|
1139
1335
|
}
|
|
1140
1336
|
|
|
1337
|
+
const graphqlStep = stepDefs.find((s) => s.key === 'graphql');
|
|
1141
1338
|
if (!skipGraphql) {
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1339
|
+
await runStep(graphqlStep, targetOrg, () => {
|
|
1340
|
+
run('UI Bundle npm install', 'npm', ['install'], { cwd: uiBundleDir });
|
|
1341
|
+
run('GraphQL schema (introspect)', 'npm', ['run', 'graphql:schema'], {
|
|
1342
|
+
cwd: uiBundleDir,
|
|
1343
|
+
env: { ...process.env, SF_TARGET_ORG: targetOrg },
|
|
1344
|
+
});
|
|
1345
|
+
run('GraphQL codegen', 'npm', ['run', 'graphql:codegen'], { cwd: uiBundleDir });
|
|
1346
|
+
run('UI Bundle build (post-codegen)', 'npm', ['run', 'build'], { cwd: uiBundleDir });
|
|
1347
|
+
});
|
|
1348
|
+
} else {
|
|
1349
|
+
recordSkipped(graphqlStep, 'not selected');
|
|
1350
|
+
if (!skipUIBundleBuild && skipDeploy && !preDeployBundlesBuilt) {
|
|
1351
|
+
// The pre-deploy build never ran (deploy was skipped); build here and
|
|
1352
|
+
// record the uiBundleBuild outcome that the pre-deploy branch would have.
|
|
1353
|
+
await runStep(uiBundleBuildStep, targetOrg, () => {
|
|
1354
|
+
run('UI Bundle npm install', 'npm', ['install'], { cwd: uiBundleDir });
|
|
1355
|
+
run('UI Bundle build', 'npm', ['run', 'build'], { cwd: uiBundleDir });
|
|
1356
|
+
});
|
|
1357
|
+
preDeployBundlesBuilt = true;
|
|
1358
|
+
}
|
|
1151
1359
|
}
|
|
1152
1360
|
|
|
1153
|
-
|
|
1361
|
+
// When uiBundleBuild was selected but the dedicated pre-deploy build never ran
|
|
1362
|
+
// (deploy skipped and graphql selected), the build happened inside the graphql
|
|
1363
|
+
// step — which is fail-fast, so reaching here means it succeeded. Record ok.
|
|
1364
|
+
if (!skipUIBundleBuild && !preDeployBundlesBuilt && !results.some((r) => r.key === 'uiBundleBuild')) {
|
|
1365
|
+
recordOk(uiBundleBuildStep);
|
|
1366
|
+
}
|
|
1154
1367
|
|
|
1368
|
+
const devStep = stepDefs.find((s) => s.key === 'dev');
|
|
1155
1369
|
if (!skipDev) {
|
|
1370
|
+
// dev is skippable and terminal: a failure here is a local runtime issue
|
|
1371
|
+
// (port in use, tooling), not a broken setup. Print the summary BEFORE the
|
|
1372
|
+
// (blocking, long-lived) dev server starts so the developer sees the setup
|
|
1373
|
+
// outcome up front; the server then runs in the foreground until Ctrl+C.
|
|
1374
|
+
recordOk(devStep);
|
|
1375
|
+
printSummary(targetOrg);
|
|
1156
1376
|
console.log('\n--- Launching dev server (Ctrl+C to stop) ---\n');
|
|
1157
|
-
run('Dev server', 'npm', ['run', 'dev'], { cwd: uiBundleDir });
|
|
1377
|
+
const devResult = run('Dev server', 'npm', ['run', 'dev'], { cwd: uiBundleDir, optional: true });
|
|
1378
|
+
if (devResult.status !== 0) {
|
|
1379
|
+
// Replace the optimistic `ok` with a skippable failure and reprint.
|
|
1380
|
+
const row = results.find((r) => r.key === 'dev');
|
|
1381
|
+
row.status = 'failed';
|
|
1382
|
+
row.reason = 'dev server failed to start — setup itself completed; check local runtime';
|
|
1383
|
+
printSummary(targetOrg);
|
|
1384
|
+
}
|
|
1385
|
+
process.exit(finalExitCode());
|
|
1386
|
+
} else {
|
|
1387
|
+
recordSkipped(devStep, 'not selected');
|
|
1158
1388
|
}
|
|
1389
|
+
|
|
1390
|
+
printSummary(targetOrg);
|
|
1391
|
+
process.exit(finalExitCode());
|
|
1159
1392
|
}
|
|
1160
1393
|
|
|
1161
1394
|
main().catch((err) => {
|