@salesforce/ui-bundle-template-app-react-sample-b2x 10.23.0 → 10.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CHANGELOG.md +9 -0
- package/dist/README.md +19 -9
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/package.json +4 -4
- package/dist/package-lock.json +2 -2
- package/dist/package.json +5 -1
- package/dist/scripts/org-setup-config-schema.mjs +96 -0
- package/dist/scripts/org-setup.config.json +2 -5
- package/dist/scripts/org-setup.mjs +408 -178
- package/dist/scripts/validate-org-setup-config.mjs +38 -0
- package/package.json +1 -1
package/dist/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [10.24.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v10.23.0...v10.24.0) (2026-06-25)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **org-setup:** error handling, result ledger, and config validation @W-23043729 ([#665](https://github.com/salesforce-experience-platform-emu/webapps/issues/665)) ([f68ff9c](https://github.com/salesforce-experience-platform-emu/webapps/commit/f68ff9cf68df52efba1a9bea25f22f30246cb083))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
6
15
|
## [10.23.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v10.22.0...v10.23.0) (2026-06-25)
|
|
7
16
|
|
|
8
17
|
**Note:** Version bump only for package @salesforce/ui-bundle-template-base-sfdx-project
|
package/dist/README.md
CHANGED
|
@@ -133,8 +133,7 @@ The `npm run setup` script reads `scripts/org-setup.config.json` to control whic
|
|
|
133
133
|
"Property_Management_Access": { "assignee": "currentUser" },
|
|
134
134
|
"Tenant_Maintenance_Access": { "assignee": "skip" },
|
|
135
135
|
"Property_Rental_Guest_User_Access": {
|
|
136
|
-
"assignee": "guestUser"
|
|
137
|
-
"siteName": "propertyrentalapp"
|
|
136
|
+
"assignee": "guestUser"
|
|
138
137
|
}
|
|
139
138
|
}
|
|
140
139
|
},
|
|
@@ -143,7 +142,6 @@ The `npm run setup` script reads `scripts/org-setup.config.json` to control whic
|
|
|
143
142
|
"roleName": "Admin"
|
|
144
143
|
},
|
|
145
144
|
"selfRegistration": {
|
|
146
|
-
"siteName": "propertyrentalapp",
|
|
147
145
|
"selfRegProfile": "Property Rental Prospect Profile",
|
|
148
146
|
"accountName": "Property Rental Self-Registration"
|
|
149
147
|
}
|
|
@@ -154,12 +152,13 @@ The `npm run setup` script reads `scripts/org-setup.config.json` to control whic
|
|
|
154
152
|
|
|
155
153
|
Each key is a permission set API name. The `assignee` value controls who it is assigned to:
|
|
156
154
|
|
|
157
|
-
| Value
|
|
158
|
-
|
|
|
159
|
-
| `"currentUser"`
|
|
160
|
-
| `"
|
|
161
|
-
| `"
|
|
162
|
-
|
|
155
|
+
| Value | Behavior |
|
|
156
|
+
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
157
|
+
| `"currentUser"` | Assigns to the user running the script (resolved via `sf org display`) |
|
|
158
|
+
| `"guestUser"` | Auto-resolves the site's guest user. The site is derived from the single `networks/<siteName>.network-meta.xml` the app ships — it is **not** configured per assignment |
|
|
159
|
+
| `"skip"` | Explicitly skips this permission set |
|
|
160
|
+
|
|
161
|
+
`assignee` is a closed set — only the three values above are accepted. Arbitrary usernames are not supported.
|
|
163
162
|
|
|
164
163
|
#### `role`
|
|
165
164
|
|
|
@@ -174,6 +173,17 @@ Enables Experience Cloud self-registration by:
|
|
|
174
173
|
3. Creating an Account record for self-registered users
|
|
175
174
|
4. Creating the `NetworkSelfRegistration` record
|
|
176
175
|
|
|
176
|
+
Like `guestUser` permset assignment, the site is derived from the single `networks/<siteName>.network-meta.xml` the app ships — `selfRegistration` does **not** take a `siteName`.
|
|
177
|
+
|
|
178
|
+
#### Validation
|
|
179
|
+
|
|
180
|
+
`org-setup.config.json` is validated against a shared schema at **two moments**, so a malformed config can never silently misbehave:
|
|
181
|
+
|
|
182
|
+
- **Build / CI time** — every shipped config is validated; an invalid one fails the build.
|
|
183
|
+
- **Setup runtime** — `npm run setup` validates the config before any step runs and exits early with a clear error if it's invalid.
|
|
184
|
+
|
|
185
|
+
Validation is **strict**: unknown keys (e.g. a `siteName` on an assignment or on `selfRegistration`, or a `permsetAssignment` typo) are rejected rather than silently ignored.
|
|
186
|
+
|
|
177
187
|
> **After the automated setup completes**, proceed to [Org Configuration](#org-configuration) for the manual steps that cannot be automated via CLI (profile cloning, site member configuration, guest user setup, and publishing the Experience Cloud site).
|
|
178
188
|
|
|
179
189
|
---
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"graphql:schema": "node scripts/get-graphql-schema.mjs"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@salesforce/platform-sdk": "^10.
|
|
22
|
-
"@salesforce/ui-bundle": "^10.
|
|
21
|
+
"@salesforce/platform-sdk": "^10.24.0",
|
|
22
|
+
"@salesforce/ui-bundle": "^10.24.0",
|
|
23
23
|
"@tailwindcss/vite": "^4.1.17",
|
|
24
24
|
"class-variance-authority": "^0.7.1",
|
|
25
25
|
"clsx": "^2.1.1",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"@graphql-eslint/eslint-plugin": "^4.1.0",
|
|
51
51
|
"@graphql-tools/utils": "^11.0.0",
|
|
52
52
|
"@playwright/test": "^1.49.0",
|
|
53
|
-
"@salesforce/graphiti": "^10.
|
|
54
|
-
"@salesforce/vite-plugin-ui-bundle": "^10.
|
|
53
|
+
"@salesforce/graphiti": "^10.24.0",
|
|
54
|
+
"@salesforce/vite-plugin-ui-bundle": "^10.24.0",
|
|
55
55
|
"@testing-library/jest-dom": "^6.6.3",
|
|
56
56
|
"@testing-library/react": "^16.1.0",
|
|
57
57
|
"@testing-library/user-event": "^14.5.2",
|
package/dist/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/webapp-template-base-sfdx-project-experimental",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.24.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@salesforce/webapp-template-base-sfdx-project-experimental",
|
|
9
|
-
"version": "10.
|
|
9
|
+
"version": "10.24.0",
|
|
10
10
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
11
11
|
"devDependencies": {
|
|
12
12
|
"@lwc/eslint-plugin-lwc": "^3.3.0",
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/ui-bundle-template-base-sfdx-project",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.24.0",
|
|
4
4
|
"description": "Base SFDX project template",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"publishConfig": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"build": "echo 'No build required for base-sfdx-project'",
|
|
12
12
|
"clean": "echo 'No clean required for base-sfdx-project'",
|
|
13
13
|
"lint": "eslint --no-error-on-unmatched-pattern **/{aura,lwc}/**/*.js",
|
|
14
|
+
"validate:org-setup-config": "node scripts/validate-org-setup-config.mjs",
|
|
14
15
|
"test": "npm run test:unit",
|
|
15
16
|
"test:coverage": "npm run test",
|
|
16
17
|
"test:unit": "sfdx-lwc-jest -- --passWithNoTests",
|
|
@@ -22,6 +23,9 @@
|
|
|
22
23
|
"precommit": "lint-staged",
|
|
23
24
|
"setup": "node scripts/org-setup.mjs"
|
|
24
25
|
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"zod": "^3.24.1"
|
|
28
|
+
},
|
|
25
29
|
"devDependencies": {
|
|
26
30
|
"@lwc/eslint-plugin-lwc": "^3.3.0",
|
|
27
31
|
"@prettier/plugin-xml": "^3.2.2",
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared schema + validator for org-setup.config.json (W-23043729).
|
|
3
|
+
*
|
|
4
|
+
* Authored ONCE, used at TWO moments:
|
|
5
|
+
* (a) build / CI time — `scripts/validate-org-setup-config.mjs` validates every
|
|
6
|
+
* org-setup.config.json this repo ships, failing the build on any violation.
|
|
7
|
+
* (b) setup runtime — `org-setup.mjs` calls `validateConfig` once in main(),
|
|
8
|
+
* before any step runs, to catch the developer's local post-scaffold edits.
|
|
9
|
+
*
|
|
10
|
+
* `validateConfig` is pure: it parses + validates and RETURNS a result. Each call
|
|
11
|
+
* site owns its own print/exit, so the build gate can report every bad file and
|
|
12
|
+
* the runtime can exit immediately — and the validator stays unit-testable.
|
|
13
|
+
*
|
|
14
|
+
* The zod schema uses `.strict()` at every level so unknown keys (e.g. the
|
|
15
|
+
* `permsetAssignment` singular typo) are rejected rather than silently ignored.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
|
|
20
|
+
/** Closed set of assignee values — no arbitrary usernames. */
|
|
21
|
+
const Assignee = z.enum(['currentUser', 'guestUser', 'skip']);
|
|
22
|
+
|
|
23
|
+
// No siteName: for guestUser the site is derived from the single
|
|
24
|
+
// networks/<siteName>.network-meta.xml the app ships (see spec §5.2).
|
|
25
|
+
const Assignment = z
|
|
26
|
+
.object({
|
|
27
|
+
assignee: Assignee,
|
|
28
|
+
})
|
|
29
|
+
.strict();
|
|
30
|
+
|
|
31
|
+
export const ConfigSchema = z
|
|
32
|
+
.object({
|
|
33
|
+
permsetAssignments: z
|
|
34
|
+
.object({
|
|
35
|
+
// guestUser is valid here — siteName is derived, not per-assignment.
|
|
36
|
+
defaultAssignee: Assignee.default('skip'),
|
|
37
|
+
assignments: z.record(z.string(), Assignment).default({}),
|
|
38
|
+
})
|
|
39
|
+
.strict()
|
|
40
|
+
.optional(),
|
|
41
|
+
role: z
|
|
42
|
+
.object({
|
|
43
|
+
assignee: z.literal('currentUser'), // only value the role code honors
|
|
44
|
+
roleName: z.string().min(1),
|
|
45
|
+
})
|
|
46
|
+
.strict()
|
|
47
|
+
.optional(),
|
|
48
|
+
selfRegistration: z
|
|
49
|
+
.object({
|
|
50
|
+
// No siteName: the site is derived from the single
|
|
51
|
+
// networks/<siteName>.network-meta.xml the app ships (see spec §5.2),
|
|
52
|
+
// exactly like the guestUser permset path.
|
|
53
|
+
selfRegProfile: z.string().min(1),
|
|
54
|
+
accountName: z.string().min(1),
|
|
55
|
+
})
|
|
56
|
+
.strict()
|
|
57
|
+
.optional(),
|
|
58
|
+
})
|
|
59
|
+
.strict();
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Render a single zod issue as a "path: message" line. Empty path (a root-level
|
|
63
|
+
* problem) renders as "<root>: message".
|
|
64
|
+
*/
|
|
65
|
+
function formatIssue(issue) {
|
|
66
|
+
const path = issue.path.length > 0 ? issue.path.join('.') : '<root>';
|
|
67
|
+
return `${path}: ${issue.message}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parse + validate a raw org-setup.config.json string.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} rawText raw file contents (not yet JSON-parsed)
|
|
74
|
+
* @param {string} [configPath] path used only for error messages
|
|
75
|
+
* @returns {{ ok: true, data: object } | { ok: false, errors: string[] }}
|
|
76
|
+
* On success, `data` is the parsed + defaulted config. On failure, `errors`
|
|
77
|
+
* is a non-empty list of human-readable messages (JSON parse error or zod
|
|
78
|
+
* issues). The caller decides whether to print/exit.
|
|
79
|
+
*/
|
|
80
|
+
export function validateConfig(rawText, configPath) {
|
|
81
|
+
const where = configPath ? ` (${configPath})` : '';
|
|
82
|
+
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = JSON.parse(rawText);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
return { ok: false, errors: [`malformed JSON${where}: ${err.message}`] };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const result = ConfigSchema.safeParse(parsed);
|
|
91
|
+
if (!result.success) {
|
|
92
|
+
return { ok: false, errors: result.error.issues.map(formatIssue) };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { ok: true, data: result.data };
|
|
96
|
+
}
|
|
@@ -8,12 +8,10 @@
|
|
|
8
8
|
"assignee": "skip"
|
|
9
9
|
},
|
|
10
10
|
"Property_Rental_Guest_User_Access": {
|
|
11
|
-
"assignee": "guestUser"
|
|
12
|
-
"siteName": "propertyrentalapp"
|
|
11
|
+
"assignee": "guestUser"
|
|
13
12
|
},
|
|
14
13
|
"propertyrentalapp_Guest_User_Api_Access": {
|
|
15
|
-
"assignee": "guestUser"
|
|
16
|
-
"siteName": "propertyrentalapp"
|
|
14
|
+
"assignee": "guestUser"
|
|
17
15
|
}
|
|
18
16
|
}
|
|
19
17
|
},
|
|
@@ -22,7 +20,6 @@
|
|
|
22
20
|
"roleName": "Admin"
|
|
23
21
|
},
|
|
24
22
|
"selfRegistration": {
|
|
25
|
-
"siteName": "propertyrentalapp",
|
|
26
23
|
"selfRegProfile": "Property Rental Prospect Profile",
|
|
27
24
|
"accountName": "Property Rental Self-Registration"
|
|
28
25
|
}
|
|
@@ -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
|
+
|
|
231
|
+
/**
|
|
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
|
+
|
|
215
277
|
/**
|
|
216
|
-
*
|
|
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
|
});
|
|
@@ -777,8 +815,7 @@ function run(name, cmd, args, opts = {}) {
|
|
|
777
815
|
...(opts.timeout && { timeout: opts.timeout }),
|
|
778
816
|
});
|
|
779
817
|
if (result.status !== 0 && !optional) {
|
|
780
|
-
|
|
781
|
-
process.exit(result.status ?? 1);
|
|
818
|
+
throw new StepError(`${name} (exit ${result.status ?? 1})`);
|
|
782
819
|
}
|
|
783
820
|
return result;
|
|
784
821
|
}
|
|
@@ -808,12 +845,91 @@ async function runAsync(name, cmd, args, opts = {}) {
|
|
|
808
845
|
if (result.status !== 0 && !optional) {
|
|
809
846
|
if (result.stdout) process.stdout.write(result.stdout);
|
|
810
847
|
if (result.stderr) process.stderr.write(result.stderr);
|
|
811
|
-
|
|
812
|
-
process.exit(result.status ?? 1);
|
|
848
|
+
throw new StepError(`${name} (exit ${result.status ?? 1})`);
|
|
813
849
|
}
|
|
814
850
|
return result;
|
|
815
851
|
}
|
|
816
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
|
+
|
|
817
933
|
async function main() {
|
|
818
934
|
// Ensure .gitignore files exist (npm strips them from published packages).
|
|
819
935
|
const gitignoreTemplates = loadGitignoreTemplates();
|
|
@@ -853,22 +969,30 @@ async function main() {
|
|
|
853
969
|
? `Permset — assign ${permsetNames.join(', ')}`
|
|
854
970
|
: `Permset — assign ${permsetNames.length} permission sets`;
|
|
855
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
|
+
|
|
856
976
|
const hasDataPlan = existsSync(DATA_PLAN) && existsSync(DATA_DIR);
|
|
857
|
-
const roleConfig = loadRoleConfig();
|
|
977
|
+
const roleConfig = loadRoleConfig(config);
|
|
858
978
|
const hasRoleConfig = roleConfig !== null;
|
|
859
|
-
const selfRegConfig = loadSelfRegConfig();
|
|
979
|
+
const selfRegConfig = loadSelfRegConfig(config);
|
|
860
980
|
const hasSelfRegConfig = selfRegConfig !== null;
|
|
861
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.
|
|
862
986
|
const stepDefs = [
|
|
863
|
-
{ key: 'login', label: 'Login — org authentication', enabled: !argSkipLogin, available: true },
|
|
864
|
-
{ key: 'uiBundleBuild', label: 'UI Bundle Build — npm install + build (pre-deploy)', enabled: !argSkipUIBundleBuild, available: true },
|
|
865
|
-
{ key: 'deploy', label: 'Deploy — sf project deploy start', enabled: !argSkipDeploy, available: true },
|
|
866
|
-
{ key: 'permset', label: permsetStepLabel, enabled: !argSkipPermset, available: true },
|
|
867
|
-
{ key: 'role', label: `Role — assign "${roleConfig?.roleName ?? '?'}" to current user`, enabled: !argSkipRole && hasRoleConfig, available: hasRoleConfig },
|
|
868
|
-
{ key: 'selfReg', label:
|
|
869
|
-
{ key: 'data', label: 'Data — delete + import records via Apex', enabled: !argSkipData && hasDataPlan, available: hasDataPlan },
|
|
870
|
-
{ key: 'graphql', label: 'GraphQL — schema introspect + codegen', enabled: !argSkipGraphql, available: true },
|
|
871
|
-
{ 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 },
|
|
872
996
|
];
|
|
873
997
|
|
|
874
998
|
const selections = yes ? stepDefs.map((s) => s.enabled) : await promptSteps(stepDefs);
|
|
@@ -905,48 +1029,94 @@ async function main() {
|
|
|
905
1029
|
!skipDev
|
|
906
1030
|
);
|
|
907
1031
|
|
|
1032
|
+
const loginStep = stepDefs.find((s) => s.key === 'login');
|
|
908
1033
|
if (!skipLogin) {
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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');
|
|
915
1046
|
}
|
|
916
1047
|
|
|
917
1048
|
// Ensure the self-reg profile is in networkMemberGroups before deploy so that
|
|
918
|
-
// 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.
|
|
919
1054
|
if (!skipDeploy && selfRegConfig) {
|
|
920
|
-
|
|
921
|
-
|
|
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
|
+
}
|
|
922
1065
|
}
|
|
923
1066
|
|
|
924
1067
|
// Build all UI Bundles before deploy so dist exists for entity deployment
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
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;
|
|
931
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');
|
|
932
1086
|
}
|
|
933
1087
|
|
|
1088
|
+
const deployStep = stepDefs.find((s) => s.key === 'deploy');
|
|
934
1089
|
if (!skipDeploy) {
|
|
935
|
-
|
|
936
|
-
|
|
1090
|
+
await runStep(deployStep, targetOrg, () => {
|
|
1091
|
+
run('Deploy metadata', 'sf', ['project', 'deploy', 'start', '--target-org', targetOrg], {
|
|
1092
|
+
timeout: 180000,
|
|
1093
|
+
});
|
|
937
1094
|
});
|
|
1095
|
+
} else {
|
|
1096
|
+
recordSkipped(deployStep, 'not selected');
|
|
938
1097
|
}
|
|
939
1098
|
|
|
1099
|
+
const permsetStep = stepDefs.find((s) => s.key === 'permset');
|
|
940
1100
|
if (!skipPermset) {
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
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
|
+
}
|
|
946
1108
|
console.log('\n--- Assign permission sets ---');
|
|
947
1109
|
|
|
948
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.
|
|
949
1118
|
const assignmentJobs = [];
|
|
1119
|
+
const resolutionFailures = [];
|
|
950
1120
|
for (const permsetName of permsetNames) {
|
|
951
1121
|
const assignment = resolveAssignment(permsetName, permsetConfig);
|
|
952
1122
|
if (assignment.assignee === 'skip') {
|
|
@@ -955,18 +1125,21 @@ async function main() {
|
|
|
955
1125
|
}
|
|
956
1126
|
let effectiveUsername = null;
|
|
957
1127
|
if (assignment.assignee === 'guestUser') {
|
|
958
|
-
|
|
959
|
-
|
|
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)`);
|
|
960
1134
|
continue;
|
|
961
1135
|
}
|
|
962
|
-
effectiveUsername = resolveGuestUsername(
|
|
1136
|
+
effectiveUsername = resolveGuestUsername(siteName, targetOrg);
|
|
963
1137
|
if (!effectiveUsername) {
|
|
964
|
-
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}")`);
|
|
965
1140
|
continue;
|
|
966
1141
|
}
|
|
967
|
-
console.log(` Resolved guest user for site "${
|
|
968
|
-
} else if (assignment.assignee !== 'currentUser') {
|
|
969
|
-
effectiveUsername = assignment.assignee;
|
|
1142
|
+
console.log(` Resolved guest user for site "${siteName}": ${effectiveUsername}`);
|
|
970
1143
|
}
|
|
971
1144
|
assignmentJobs.push({ permsetName, effectiveUsername });
|
|
972
1145
|
}
|
|
@@ -982,6 +1155,7 @@ async function main() {
|
|
|
982
1155
|
return { permsetName, assigneeLabel, result };
|
|
983
1156
|
}));
|
|
984
1157
|
|
|
1158
|
+
const failures = [];
|
|
985
1159
|
for (const { permsetName, assigneeLabel, result } of assignResults) {
|
|
986
1160
|
if (result.status === 0) {
|
|
987
1161
|
console.log(`Permission set "${permsetName}" assigned to ${assigneeLabel}.`);
|
|
@@ -994,35 +1168,55 @@ async function main() {
|
|
|
994
1168
|
} else {
|
|
995
1169
|
if (result.stdout) process.stdout.write(result.stdout);
|
|
996
1170
|
if (result.stderr) process.stderr.write(result.stderr);
|
|
997
|
-
|
|
998
|
-
process.exit(result.status ?? 1);
|
|
1171
|
+
failures.push(`${permsetName} (exit ${result.status ?? 1})`);
|
|
999
1172
|
}
|
|
1000
1173
|
}
|
|
1001
1174
|
}
|
|
1002
|
-
|
|
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');
|
|
1003
1182
|
}
|
|
1004
1183
|
|
|
1184
|
+
const roleStep = stepDefs.find((s) => s.key === 'role');
|
|
1005
1185
|
if (!skipRole) {
|
|
1006
1186
|
console.log('\n--- Assign role ---');
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
} 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, () => {
|
|
1012
1191
|
assignRoleToCurrentUser(roleConfig.roleName, targetOrg);
|
|
1013
|
-
}
|
|
1192
|
+
});
|
|
1193
|
+
} else {
|
|
1194
|
+
recordSkipped(roleStep, roleStep.available ? 'not selected' : 'no config');
|
|
1014
1195
|
}
|
|
1015
1196
|
|
|
1197
|
+
const selfRegStep = stepDefs.find((s) => s.key === 'selfReg');
|
|
1016
1198
|
if (!skipSelfReg) {
|
|
1017
1199
|
console.log('\n--- Enable self-registration ---');
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
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');
|
|
1023
1215
|
}
|
|
1024
1216
|
|
|
1217
|
+
const dataStep = stepDefs.find((s) => s.key === 'data');
|
|
1025
1218
|
if (doData) {
|
|
1219
|
+
await runStep(dataStep, targetOrg, () => {
|
|
1026
1220
|
// Prepare data for uniqueness (run before import so repeat imports don't conflict)
|
|
1027
1221
|
const prepareScript = resolve(__dirname, 'prepare-import-unique-fields.js');
|
|
1028
1222
|
run('Prepare data (unique fields)', 'node', [prepareScript, '--data-dir', DATA_DIR], {
|
|
@@ -1114,9 +1308,8 @@ async function main() {
|
|
|
1114
1308
|
const apexOut = apexResult.stdout?.toString() || '';
|
|
1115
1309
|
const apexErr = apexResult.stderr?.toString() || '';
|
|
1116
1310
|
if (apexResult.status !== 0 && !apexOut.includes('Compiled successfully')) {
|
|
1117
|
-
console.error(` ${entry.sobject}: apex execution failed`);
|
|
1118
1311
|
process.stderr.write(apexErr || apexOut);
|
|
1119
|
-
|
|
1312
|
+
throw new StepError(`${entry.sobject}: apex execution failed`);
|
|
1120
1313
|
}
|
|
1121
1314
|
const okMatches = [...apexOut.matchAll(/\|DEBUG\|REF:([^:\n]+):(\w+)/g)];
|
|
1122
1315
|
const errMatches = [...apexOut.matchAll(/\|DEBUG\|ERR:([^:\n]+):([^\n]+)/g)];
|
|
@@ -1125,8 +1318,7 @@ async function main() {
|
|
|
1125
1318
|
console.error(` ${m[1]}: ${m[2].trim()}`);
|
|
1126
1319
|
}
|
|
1127
1320
|
if (errMatches.length > 5) console.error(` ... and ${errMatches.length - 5} more`);
|
|
1128
|
-
|
|
1129
|
-
process.exit(1);
|
|
1321
|
+
throw new StepError(`data import tree (${entry.sobject}) — ${errMatches.length} record error(s)`);
|
|
1130
1322
|
}
|
|
1131
1323
|
if (entry.saveRefs) {
|
|
1132
1324
|
for (const m of okMatches) refMap.set(m[1], m[2]);
|
|
@@ -1137,28 +1329,66 @@ async function main() {
|
|
|
1137
1329
|
}
|
|
1138
1330
|
}
|
|
1139
1331
|
if (existsSync(tmpApex)) unlinkSync(tmpApex);
|
|
1332
|
+
});
|
|
1333
|
+
} else {
|
|
1334
|
+
recordSkipped(dataStep, dataStep.available ? 'not selected' : 'no data plan');
|
|
1140
1335
|
}
|
|
1141
1336
|
|
|
1337
|
+
const graphqlStep = stepDefs.find((s) => s.key === 'graphql');
|
|
1142
1338
|
if (!skipGraphql) {
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
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 });
|
|
1147
1347
|
});
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
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
|
+
}
|
|
1154
1359
|
}
|
|
1155
1360
|
|
|
1156
|
-
|
|
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
|
+
}
|
|
1157
1367
|
|
|
1368
|
+
const devStep = stepDefs.find((s) => s.key === 'dev');
|
|
1158
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);
|
|
1159
1376
|
console.log('\n--- Launching dev server (Ctrl+C to stop) ---\n');
|
|
1160
|
-
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');
|
|
1161
1388
|
}
|
|
1389
|
+
|
|
1390
|
+
printSummary(targetOrg);
|
|
1391
|
+
process.exit(finalExitCode());
|
|
1162
1392
|
}
|
|
1163
1393
|
|
|
1164
1394
|
main().catch((err) => {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build / CI gate: validate this package's org-setup.config.json against the
|
|
3
|
+
* shared schema (W-23043729, spec §5.2).
|
|
4
|
+
*
|
|
5
|
+
* Ships inside the SFDX project template, so a generated app can self-check its
|
|
6
|
+
* org-setup.config.json in its own CI:
|
|
7
|
+
* npm run validate:org-setup-config
|
|
8
|
+
*
|
|
9
|
+
* Uses the SAME validateConfig + schema as org-setup.mjs runtime, so build-time
|
|
10
|
+
* and runtime checks can never drift. A missing config file is OK (not every
|
|
11
|
+
* project ships one); a present-but-invalid config fails the gate.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { resolve, dirname } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
import { validateConfig } from './org-setup-config-schema.mjs';
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const configPath = resolve(__dirname, 'org-setup.config.json');
|
|
22
|
+
|
|
23
|
+
if (!existsSync(configPath)) {
|
|
24
|
+
console.log('✓ org-setup.config.json: not present (nothing to validate)');
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const result = validateConfig(readFileSync(configPath, 'utf8'), configPath);
|
|
29
|
+
if (result.ok) {
|
|
30
|
+
console.log('✓ org-setup.config.json');
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.error('❌ org-setup.config.json is invalid:');
|
|
35
|
+
for (const err of result.errors) {
|
|
36
|
+
console.error(` - ${err}`);
|
|
37
|
+
}
|
|
38
|
+
process.exit(1);
|