@salesforce/afv-skills 1.56.0 → 1.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/package.json +1 -1
  2. package/skills/commerce-b2b-open-code-components-integrate/SKILL.md +1 -1
  3. package/skills/commerce-b2b-open-code-components-replace/SKILL.md +16 -16
  4. package/skills/dx-devops-project-manage/SKILL.md +1 -1
  5. package/skills/experience-cms-content-generate/SKILL.md +1 -1
  6. package/skills/experience-cms-content-render/SKILL.md +3 -3
  7. package/skills/experience-cms-content-render/assets/angular/MediaRenderer.component.ts +35 -7
  8. package/skills/experience-cms-content-render/assets/angular/cms-item.service.ts +13 -4
  9. package/skills/experience-cms-content-render/assets/react/MediaRenderer.tsx +30 -8
  10. package/skills/experience-cms-content-render/assets/react/useCmsItem.ts +14 -4
  11. package/skills/experience-cms-content-render/assets/shared/cmsCore.types.ts +11 -1
  12. package/skills/experience-cms-content-render/assets/shared/externalRefs.ts +1 -1
  13. package/skills/experience-cms-content-render/assets/shared/mediaLabels.ts +62 -0
  14. package/skills/experience-cms-content-render/references/init-scaffold.md +13 -6
  15. package/skills/experience-content-media-stock-image-search/SKILL.md +0 -1
  16. package/skills/experience-lwc-design-generate/SKILL.md +1 -1
  17. package/skills/experience-lwc-legacy-migrate/SKILL.md +212 -0
  18. package/skills/experience-lwc-legacy-migrate/assets/lo20-host-page-template.html +192 -0
  19. package/skills/experience-lwc-legacy-migrate/references/aura-to-lwc-completeness-checklist.md +188 -0
  20. package/skills/experience-lwc-legacy-migrate/references/lightning-out-2-system-reference.md +495 -0
  21. package/skills/experience-lwc-legacy-migrate/references/lightning-out-beta-to-2-migration.md +824 -0
  22. package/skills/experience-lwc-legacy-migrate/scripts/convert-lo-names.py +94 -0
  23. package/skills/experience-lwc-legacy-migrate/scripts/validate-lo20-page.py +133 -0
  24. package/skills/experience-ui-bundle-project-generate/SKILL.md +14 -40
  25. package/skills/experience-ui-bundle-project-generate/scripts/flatten-project.mjs +33 -11
  26. package/skills/experience-ui-bundle-project-generate/scripts/generate-project.mjs +77 -0
  27. package/skills/experience-ui-bundle-project-generate/scripts/install-deps.mjs +95 -0
  28. package/skills/integration-connectivity-connected-app-configure/SKILL.md +5 -5
  29. package/skills/integration-connectivity-generate/SKILL.md +10 -10
  30. package/skills/integration-connectivity-generate/scripts/configure-named-credential.sh +2 -2
  31. package/skills/platform-apex-test-generate/SKILL.md +1 -1
  32. package/skills/platform-apex-test-run/SKILL.md +1 -1
  33. package/skills/platform-custom-field-generate/SKILL.md +1 -1
  34. package/skills/platform-custom-lightning-type-generate/SKILL.md +1 -1
  35. package/skills/platform-custom-metadata-type-generate/SKILL.md +1 -1
  36. package/skills/platform-custom-object-generate/SKILL.md +1 -1
  37. package/skills/platform-custom-report-type-generate/SKILL.md +1 -1
  38. package/skills/platform-custom-setting-generate/SKILL.md +1 -1
  39. package/skills/platform-data-and-tooling-api-context-get/SKILL.md +1 -1
  40. package/skills/platform-data-manage/SKILL.md +10 -10
  41. package/skills/platform-flexipage-generate/SKILL.md +1 -1
  42. package/skills/platform-lightning-app-coordinate/SKILL.md +1 -1
  43. package/skills/platform-metadata-api-context-get/SKILL.md +1 -1
  44. package/skills/platform-metadata-retrieve/SKILL.md +1 -1
  45. package/skills/platform-permission-set-generate/SKILL.md +1 -1
  46. package/skills/platform-report-generate/SKILL.md +1 -1
  47. package/skills/platform-salesforce-connect-adapter-generate/SKILL.md +4 -4
  48. package/skills/platform-sharing-owd-configure/SKILL.md +1 -1
  49. package/skills/platform-sharing-rules-generate/SKILL.md +1 -1
  50. package/skills/platform-soql-query/SKILL.md +1 -1
  51. package/skills/platform-value-set-generate/SKILL.md +1 -1
  52. package/skills/service-agentforce-human-escalation-configure/SKILL.md +4 -0
  53. package/skills/service-agentforce-human-escalation-configure/scripts/tests/_bootstrap.py +26 -6
  54. package/skills/service-agentforce-human-escalation-configure/scripts/tests/test_escalation_contracts.py +5 -4
  55. package/skills/service-de-waba-integrate/SKILL.md +2 -1
  56. package/skills/service-digital-engagement-deployment-configure/SKILL.md +2 -0
  57. package/skills/service-digital-engagement-deployment-configure/scripts/check-api-version.sh +29 -0
  58. package/skills/service-email-to-case-configure/SKILL.md +1 -1
  59. package/skills/service-omni-channel-setup-coordinate/SKILL.md +1 -0
  60. package/skills/service-omni-command-center-analyze/SKILL.md +1 -0
  61. package/skills/service-omni-command-center-configure/SKILL.md +92 -0
  62. package/skills/service-omni-command-center-configure/references/api-notes.md +50 -0
  63. package/skills/service-omni-command-center-configure/scripts/configure-and-report.sh +257 -0
  64. package/skills/service-omni-command-center-configure/scripts/settings_document.py +97 -0
  65. package/skills/service-omni-command-center-configure/scripts/tests/test_command_center_configure_contracts.py +206 -0
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env python3
2
+ """Deterministic Lightning Out Beta -> LO 2.0 name/URL transforms.
3
+
4
+ The Beta -> LO 2.0 migration includes a few *purely mechanical* string
5
+ conversions that must be applied identically every time (Workflow B, Step B3):
6
+
7
+ * Aura component id ``c:myComponent`` -> LWC custom-element tag ``c-my-component``
8
+ * camelCase attribute names -> kebab-case HTML attribute names
9
+ * the org My Domain -> the versioned LO 2.0 library ``<script>`` URL
10
+
11
+ This script performs those conversions so the workflow never has to hand-derive
12
+ them. It does NOT make any judgement calls (target DOM location, whether an LO
13
+ 2.0 App id is needed, placeholder values) -- those stay in the skill body.
14
+
15
+ Usage:
16
+ python3 scripts/convert-lo-names.py \
17
+ --component c:myComponent \
18
+ --attributes recordId,ownerId,showRevenue \
19
+ --my-domain https://acme.my.salesforce.com
20
+
21
+ Any of the flags may be omitted; only the requested conversions are printed.
22
+ Exit code is 0 on success, 2 on a malformed argument.
23
+ """
24
+ import argparse
25
+ import re
26
+ import sys
27
+
28
+ LO2_LIBRARY_PATH = "/lightning/lightning.out.latest/index.iife.prod.js"
29
+
30
+
31
+ def camel_to_kebab(name: str) -> str:
32
+ """myComponent / MyComponent / my_component -> my-component."""
33
+ name = name.strip().replace("_", "-")
34
+ # split camelCase / PascalCase boundaries
35
+ name = re.sub(r"([a-z0-9])([A-Z])", r"\1-\2", name)
36
+ name = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1-\2", name)
37
+ return name.lower()
38
+
39
+
40
+ def component_tag(aura_id: str) -> str:
41
+ """c:myComponent -> c-my-component (namespace preserved)."""
42
+ aura_id = aura_id.strip()
43
+ if ":" in aura_id:
44
+ ns, name = aura_id.split(":", 1)
45
+ else:
46
+ ns, name = "c", aura_id
47
+ return f"{ns.strip().lower()}-{camel_to_kebab(name)}"
48
+
49
+
50
+ def library_url(my_domain: str) -> str:
51
+ """Normalize a My Domain to the versioned LO 2.0 library URL."""
52
+ d = my_domain.strip().rstrip("/")
53
+ if not d.startswith("http"):
54
+ d = "https://" + d
55
+ # Beta hosts on *.lightning.force.com; LO 2.0 must load from *.my.salesforce.com
56
+ d = d.replace(".lightning.force.com", ".my.salesforce.com")
57
+ # strip any path the caller pasted, keep scheme://host
58
+ m = re.match(r"(https?://[^/]+)", d)
59
+ host = m.group(1) if m else d
60
+ return f"{host}{LO2_LIBRARY_PATH}"
61
+
62
+
63
+ def main(argv=None) -> int:
64
+ p = argparse.ArgumentParser(description=__doc__,
65
+ formatter_class=argparse.RawDescriptionHelpFormatter)
66
+ p.add_argument("--component", help="Beta component id, e.g. c:myComponent")
67
+ p.add_argument("--attributes",
68
+ help="comma/space separated camelCase attribute names")
69
+ p.add_argument("--my-domain",
70
+ help="org My Domain or Beta host URL")
71
+ args = p.parse_args(argv)
72
+
73
+ if not (args.component or args.attributes or args.my_domain):
74
+ p.print_help()
75
+ return 2
76
+
77
+ if args.component:
78
+ print(f"component-tag: <{component_tag(args.component)}>")
79
+
80
+ if args.attributes:
81
+ raw = [a for a in re.split(r"[,\s]+", args.attributes) if a]
82
+ pairs = [(a, camel_to_kebab(a)) for a in raw]
83
+ print("attributes:")
84
+ for src, dst in pairs:
85
+ print(f" {src} -> {dst}")
86
+
87
+ if args.my_domain:
88
+ print(f"library-url: {library_url(args.my_domain)} (add the `async` attribute)")
89
+
90
+ return 0
91
+
92
+
93
+ if __name__ == "__main__":
94
+ sys.exit(main())
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env python3
2
+ """Mechanically validate a migrated Lightning Out 2.0 host page.
3
+
4
+ The six non-negotiable LO 2.0 rules (Workflow B, Step B4) plus the core
5
+ library/component checklist items are all *deterministic code-structure*
6
+ checks. This script runs them against a migrated HTML host page so the
7
+ workflow gates on a repeatable pass/fail instead of prose interpretation.
8
+
9
+ Usage:
10
+ python3 scripts/validate-lo20-page.py <path-to-migrated.html>
11
+
12
+ Prints one PASS/FAIL line per rule. Exits 0 only when every rule passes;
13
+ exits 1 if any rule fails, 2 on a usage/IO error.
14
+ """
15
+ import re
16
+ import sys
17
+
18
+
19
+ def check(src: str):
20
+ """Return a list of (ok: bool, label: str, detail: str)."""
21
+ results = []
22
+
23
+ def add(ok, label, detail=""):
24
+ results.append((ok, label, detail))
25
+
26
+ # --- Library / Beta removal -------------------------------------------
27
+ # Locate the versioned library path, then scan back to the opening
28
+ # <script to check for `async`. We do NOT isolate the tag with [^>]*
29
+ # because placeholder URLs like https://<DOMAIN>.my.salesforce.com
30
+ # legitimately contain a '>' inside the attribute value.
31
+ lib_path = "lightning.out.latest/index.iife.prod.js"
32
+ idx = src.find(lib_path)
33
+ has_lo2_lib = idx != -1
34
+ lib_async = False
35
+ if has_lo2_lib:
36
+ start = src.rfind("<script", 0, idx)
37
+ if start != -1:
38
+ lib_async = bool(re.search(r"\basync\b", src[start:idx]))
39
+ add(has_lo2_lib and lib_async, "LO 2.0 library script tag",
40
+ "must load .../lightning.out.latest/index.iife.prod.js with `async`")
41
+
42
+ beta_lib = bool(re.search(r"lightning\.force\.com/[^\"']*lightning\.out\.js", src))
43
+ beta_calls = bool(re.search(r"\$Lightning\.(use|createComponent)", src))
44
+ add(not beta_lib and not beta_calls, "No Lightning Out Beta remnants",
45
+ "no lightning.out.js / $Lightning.use / $Lightning.createComponent")
46
+
47
+ # --- Rule 1: customElements.whenDefined must use a literal string ------
48
+ wd = re.findall(r"customElements\.whenDefined\(\s*([^)]*?)\s*\)", src)
49
+ if not wd:
50
+ add(False, "Rule 1: whenDefined literal string",
51
+ "no customElements.whenDefined(...) call found")
52
+ else:
53
+ # Require the ENTIRE trimmed argument to be a single quoted string
54
+ # literal. A prefix-only check (starts with a quote) would let
55
+ # expressions like `'c-' + componentTag` slip through.
56
+ all_literal = all(
57
+ re.fullmatch(r"""(['"]).*?\1""", arg.strip()) for arg in wd)
58
+ add(all_literal, "Rule 1: whenDefined literal string",
59
+ "argument must be a quoted string, not the `components` variable")
60
+
61
+ # --- Rule 2: mountLo20 declared with two parameters -------------------
62
+ decl = re.search(r"function\s+mountLo20\s*\(([^)]*)\)", src)
63
+ if not decl:
64
+ add(False, "Rule 2: mountLo20(frontdoorUrl, orgUrl)",
65
+ "no `function mountLo20(...)` declaration found")
66
+ else:
67
+ params = [p for p in decl.group(1).split(",") if p.strip()]
68
+ add(len(params) == 2, "Rule 2: mountLo20(frontdoorUrl, orgUrl)",
69
+ f"declaration has {len(params)} parameter(s), expected 2")
70
+
71
+ # --- Rule 3: clearCachedResult present (called before mounting) -------
72
+ add(bool(re.search(r"clearCachedResult\s*\(", src)),
73
+ "Rule 3: clearCachedResult() before mount",
74
+ "clearCachedResult() must run before every mountLo20() call")
75
+
76
+ # --- Rule 4: every mountLo20 call passes two arguments ----------------
77
+ calls = re.findall(r"(?<!function\s)\bmountLo20\s*\(([^)]*)\)", src)
78
+ # Count args for EVERY call site (the declaration is excluded by the
79
+ # lookbehind). Do NOT skip empty argument lists: a zero-arg
80
+ # `mountLo20()` has an arg count of 0, which is != 2 and must fail.
81
+ bad_calls = [c for c in calls
82
+ if len([a for a in c.split(",") if a.strip()]) != 2]
83
+ add(len(bad_calls) == 0, "Rule 4: mountLo20 calls pass two args",
84
+ f"{len(bad_calls)} call(s) do not pass exactly two arguments")
85
+
86
+ # --- Rule 5: boot() wrapper defined and invoked -----------------------
87
+ boot_def = bool(re.search(r"function\s+boot\s*\(", src))
88
+ boot_call = bool(re.search(r"(?<!function\s)\bboot\s*\(\s*\)", src))
89
+ add(boot_def and boot_call, "Rule 5: boot() wrapper",
90
+ "define function boot() and call boot() once at the end")
91
+
92
+ # --- Rule 6: component tag has no inline display style ----------------
93
+ comp_tags = re.findall(r"<c-[a-z0-9-]+[^>]*>", src)
94
+ add(len(comp_tags) > 0, "Component tag present",
95
+ "expected a <c-...> custom element in the HTML")
96
+ inline_hidden = any(re.search(r"style\s*=\s*['\"][^'\"]*display\s*:\s*none",
97
+ t) for t in comp_tags)
98
+ add(not inline_hidden, "Rule 6: no inline display:none on component tag",
99
+ "control visibility via the loading indicator, not the component tag")
100
+
101
+ return results
102
+
103
+
104
+ def main(argv=None) -> int:
105
+ argv = argv if argv is not None else sys.argv[1:]
106
+ if len(argv) != 1:
107
+ print("usage: validate-lo20-page.py <path-to-migrated.html>",
108
+ file=sys.stderr)
109
+ return 2
110
+ try:
111
+ with open(argv[0], "r", encoding="utf-8") as fh:
112
+ src = fh.read()
113
+ except OSError as e:
114
+ print(f"error: cannot read {argv[0]}: {e}", file=sys.stderr)
115
+ return 2
116
+
117
+ results = check(src)
118
+ failed = 0
119
+ for ok, label, detail in results:
120
+ mark = "PASS" if ok else "FAIL"
121
+ line = f"[{mark}] {label}"
122
+ if not ok and detail:
123
+ line += f" -- {detail}"
124
+ print(line)
125
+ if not ok:
126
+ failed += 1
127
+
128
+ print(f"\n{len(results) - failed}/{len(results)} checks passed")
129
+ return 0 if failed == 0 else 1
130
+
131
+
132
+ if __name__ == "__main__":
133
+ sys.exit(main())
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: experience-ui-bundle-project-generate
3
- description: "Generates a minimal, ready-to-develop SFDX starter project from template instead of hand-scaffolding files. Use this skill when starting a brand-new Salesforce UI bundle app (React or Angular) and the initial project must be scaffolded — trigger phrases include create, start, or scaffold a new UI bundle app, generate a starter project, or use a prebuilt/starter template. DO NOT TRIGGER when: editing, styling, or adding pages or components to an EXISTING app (use experience-ui-bundle-frontend-generate); configuring ui-bundle.json or metadata files (use experience-ui-bundle-metadata-generate); deploying to an org (use experience-ui-bundle-deploy); or when the user explicitly says they want to hand-scaffold from scratch."
3
+ description: "Generates a minimal, ready-to-develop SFDX starter project from template instead of hand-scaffolding files. Use this skill when starting a brand-new Salesforce UI bundle app (React or Angular) and the initial project must be scaffolded — trigger phrases include create, start, or scaffold a new UI bundle app, generate a starter project, or use a prebuilt/starter template. DO NOT TRIGGER when: editing, styling, or adding pages or components to an EXISTING app (use experience-ui-bundle-frontend-generate); configuring ui-bundle.json or metadata files (use experience-ui-bundle-metadata-generate); deploying to an org (use experience-ui-bundle-deploy); when the user explicitly says they want to hand-scaffold from scratch; or when creating a brand-new standalone Salesforce project that also needs full setup — relocating the session, connecting an org, setting the default, and enabling source tracking (use dx-project-create in the salesforce-development plugin)."
4
4
  metadata:
5
5
  version: "1.2"
6
6
  domains: ["Experience"]
@@ -42,39 +42,22 @@ Once the user picks, carry the chosen `--template` flag into Step 2.
42
42
 
43
43
  ## Step 2: Generate the project into the target root
44
44
 
45
- The project contents must land **directly at the target root `$DEST`** — so `sfdx-project.json` sits at `$DEST/sfdx-project.json`, with no extra wrapper subfolder. `sf template generate project` always nests its output under a `--name` subfolder, so generate into the `$DEST` dir, then move the contents from the subfolder up into `$DEST`, overwriting anything already there on conflict. Remove the empty subfolder at the end.
45
+ The project contents must land **directly at the target root `$DEST`** — so `sfdx-project.json` sits at `$DEST/sfdx-project.json`, with no extra wrapper subfolder. `sf template generate project` always nests its output under a `--name` subfolder, so `<SKILL_DIR>/scripts/generate-project.mjs` generates into `$DEST`, flattens the subfolder's contents up into `$DEST` (overwriting anything already there on conflict), and removes the now-empty subfolder — all through Node's `fs`/`child_process` APIs, so it runs the same way on Windows cmd/PowerShell as it does on macOS/Linux/Git Bash.
46
46
 
47
47
  - `<SKILL_DIR>` = the absolute path to **this skill's own directory** — the folder containing this `SKILL.md`; resolve it from the skill path in context
48
48
  - **`$NAME`** — the project name (alphanumerical only — no spaces, hyphens, underscores, or special characters). Ask the user for it. It also names the UI bundle, so it shows up inside the project.
49
49
  - **`$DEST`** — the target root directory the contents land in (use `.` for the current directory).
50
+ - **`$TEMPLATE`** — the `--template` flag value from the framework reference chosen in Step 1 (e.g. `reactinternalapp`).
50
51
 
51
- ```sh
52
- NAME=MyApp # project name the user chose; also names the UI bundle
53
- DEST=. # target root directory (the contents land directly here, no NAME/ wrapper)
54
-
55
- # the --template flag from the framework reference chosen in Step 1
56
- TEMPLATE=reactinternalapp # example placeholder — replace with the flag from your Step-1 reference
57
-
58
- mkdir -p "$DEST"
59
- sf template generate project --name "$NAME" --template "$TEMPLATE" --output-dir "$DEST"
60
-
61
- # Flatten the generated $DEST/$NAME contents up into $DEST (see <SKILL_DIR>/scripts/flatten-project.mjs).
62
- # Use the absolute skill-dir path — a relative ./scripts/ would resolve against $DEST, not the skill.
63
- node "<SKILL_DIR>/scripts/flatten-project.mjs" "$DEST/$NAME" "$DEST"
64
- rm -rf "$DEST/$NAME"
65
- ```
66
-
67
- > `<SKILL_DIR>/scripts/flatten-project.mjs` moves every generated entry (incl. dotfiles) into `$DEST`, overwriting any existing file/dir of any type on conflict while preserving unrelated files the user already had in `$DEST`. The per-entry `rmSync` + `renameSync` is what guarantees the template's files win on conflict (including a file-vs-directory type mismatch).
68
-
69
- ### Verify
70
-
71
- After generation, confirm the contents landed at the root (not in a `$NAME/` subfolder):
52
+ Run the script with the actual, literal values substituted for `$NAME`, `$DEST`, and `$TEMPLATE` — do not use shell variable assignment/interpolation (`NAME=...` / `$NAME` / `%NAME%` / `$env:NAME`) since that syntax differs across bash, cmd, and PowerShell and this command must work in all three:
72
53
 
73
54
  ```sh
74
- test -f "$DEST/sfdx-project.json" && echo "OK: project root landed" || echo "FAILED"
55
+ node "<SKILL_DIR>/scripts/generate-project.mjs" "<name>" "<dest>" "<template>"
75
56
  ```
76
57
 
77
- `sfdx-project.json` must sit at `$DEST/sfdx-project.json`. The project also contains `package.json`, `force-app/main/default/uiBundles/$NAME/` (the UI bundle), `scripts/`, `config/`, and `README.md`. See the framework reference from Step 1 for the specific bundle contents. If `sfdx-project.json` is missing or is one level down in `$DEST/$NAME/`, the flatten did not run — re-check before continuing.
58
+ The script prints `OK: project root landed at <dest> (...)` and exits 0 on success. It exits non-zero (with a clear stderr message) if `sf template generate project` fails, the generated project has no `sfdx-project.json`, or the flatten didn't leave a valid project root — stop and surface the failure rather than continuing.
59
+
60
+ `sfdx-project.json` must sit at `$DEST/sfdx-project.json`. The project also contains `package.json`, `force-app/main/default/uiBundles/$NAME/` (the UI bundle), `scripts/`, `config/`, and `README.md`. See the framework reference from Step 1 for the specific bundle contents.
78
61
 
79
62
  ## Step 3: Install dependencies (you do this — do NOT hand off uninstalled)
80
63
 
@@ -84,26 +67,17 @@ There are **multiple** `package.json` files, each needing its own install:
84
67
  - the **project root** (`$DEST/package.json`), and
85
68
  - the **UI bundle** dir under `$DEST/force-app/main/default/uiBundles/$NAME/` — this holds the toolchain the preview server loads, so it must have `node_modules` too.
86
69
 
87
- ```sh
88
- # 1. project root ($DEST was set in Step 2)
89
- ( cd "$DEST" && npm install )
70
+ `<SKILL_DIR>/scripts/install-deps.mjs` runs `npm install` for the root and for every UI bundle that has a `package.json` (via Node's `child_process` with an explicit `cwd` — no `cd &&` shell chaining), then verifies `node_modules` landed everywhere it installed:
90
71
 
91
- # 2. each UI bundle
92
- for b in "$DEST"/force-app/main/default/uiBundles/*/; do
93
- [ -f "$b/package.json" ] && ( cd "$b" && npm install )
94
- done
72
+ ```sh
73
+ node "<SKILL_DIR>/scripts/install-deps.mjs" "<dest>"
95
74
  ```
96
75
 
97
- > First-run install of the bundle is the heavy step; expect a short wait. If an install fails, surface it — don't hand off a half-installed project.
76
+ Substitute the literal `$DEST` value from Step 2 for `<dest>`. The script prints `OK: all dependencies installed.` and exits 0 on success; it exits non-zero with a listed summary of which install(s) failed otherwise. First-run install of the bundle is the heavy step; expect a short wait. If an install fails, surface it — don't hand off a half-installed project.
98
77
 
99
78
  ## Step 4: Confirm and hand off
100
79
 
101
- Verify the project landed and is installed:
102
-
103
- ```sh
104
- ls "$DEST" # sfdx-project.json, package.json, force-app/, scripts/, README.md ...
105
- ls "$DEST"/force-app/main/default/uiBundles/*/node_modules >/dev/null && echo "bundle deps installed"
106
- ```
80
+ `install-deps.mjs` already prints a verification summary (root + each UI bundle's `node_modules`) as part of Step 3. To look around the generated project yourself, use your own file-listing/read tools rather than a shell `ls` — that works identically regardless of the underlying OS shell.
107
81
 
108
82
  The project is now ready to develop and deploy. If there's a `README.md` in the template, take a look at it to see if there is any extra step or guidance for the user.
109
83
 
@@ -113,4 +87,4 @@ From here, continue development with the other ui-bundle skills (`experience-ui-
113
87
 
114
88
  - The starters are **minimal** — no seeded sample data or custom objects. Build the rest with the other ui-bundle skills.
115
89
  - These templates use the `uiBundles` metadata convention. The UI bundle directory and meta XML are named after the project name you pass to `--name`.
116
- - `sf template generate project --help` lists all available templates if the flag names ever change.
90
+ - `sf template generate project --help` lists all available templates if the flag names ever change.
@@ -10,24 +10,46 @@
10
10
  // — only the paths the template ships get replaced. The per-entry rmSync + renameSync is what
11
11
  // guarantees the template's files win on conflict (including a file-vs-directory type mismatch).
12
12
  // Requires Node ≥ 16.7 for rmSync.
13
+ //
14
+ // Always invoke via `node` (never as a bare executable) so this works on Windows cmd/PowerShell
15
+ // as well as macOS/Linux/Git Bash. Uses fs/path APIs only — no shell-out — so path separators and
16
+ // move semantics are correct on every OS.
13
17
 
14
18
  import fs from "node:fs";
15
19
  import path from "node:path";
20
+ import { pathToFileURL } from "node:url";
16
21
 
17
- const [src, dest] = process.argv.slice(2);
22
+ export function flattenProject(src, dest) {
23
+ if (!fs.existsSync(path.join(src, "sfdx-project.json"))) {
24
+ throw new Error("generated project has no sfdx-project.json: " + src);
25
+ }
18
26
 
19
- if (!src || !dest) {
20
- console.error("usage: node <skill_dir>/scripts/flatten-project.mjs <srcDir> <destDir>");
21
- process.exit(1);
27
+ for (const entry of fs.readdirSync(src)) {
28
+ const target = path.join(dest, entry);
29
+ fs.rmSync(target, { recursive: true, force: true });
30
+ fs.renameSync(path.join(src, entry), target);
31
+ }
22
32
  }
23
33
 
24
- if (!fs.existsSync(path.join(src, "sfdx-project.json"))) {
25
- console.error("generated project has no sfdx-project.json: " + src);
26
- process.exit(1);
34
+ function main() {
35
+ const [src, dest] = process.argv.slice(2);
36
+
37
+ if (!src || !dest) {
38
+ console.error("usage: node <skill_dir>/scripts/flatten-project.mjs <srcDir> <destDir>");
39
+ process.exit(1);
40
+ }
41
+
42
+ try {
43
+ flattenProject(src, dest);
44
+ } catch (err) {
45
+ console.error(err.message);
46
+ process.exit(1);
47
+ }
27
48
  }
28
49
 
29
- for (const entry of fs.readdirSync(src)) {
30
- const target = path.join(dest, entry);
31
- fs.rmSync(target, { recursive: true, force: true });
32
- fs.renameSync(path.join(src, entry), target);
50
+ // Only run as a CLI when invoked directly (not when imported by generate-project.mjs).
51
+ // pathToFileURL handles Windows drive letters/backslashes correctly (a manual
52
+ // `file://${path}` string would not).
53
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
54
+ main();
33
55
  }
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ // Generate a `sf template generate project` starter and flatten it so the
3
+ // contents land directly at the target root (no `<name>/` wrapper subfolder).
4
+ //
5
+ // Usage: node <skill_dir>/scripts/generate-project.mjs <name> <dest> <template>
6
+ // <name> — project name passed to `sf template generate project --name`
7
+ // <dest> — target root directory the contents should land in
8
+ // (so sfdx-project.json sits at <dest>/sfdx-project.json)
9
+ // <template> — the `--template` flag value from the chosen framework reference
10
+ //
11
+ // Always invoke via `node` (never as a bare executable, and never wrap this
12
+ // in a bash/cmd/PowerShell script) so this works on Windows cmd/PowerShell
13
+ // as well as macOS/Linux/Git Bash. Pass <name>/<dest>/<template> as literal
14
+ // argv values — do not rely on shell variable assignment/interpolation
15
+ // (`$VAR` / `%VAR%` / `$env:VAR` differ per shell).
16
+ //
17
+ // Runs `sf` via spawnSync with shell:true so the platform's own shell
18
+ // resolves the CLI shim correctly (on Windows, globally-installed CLIs are
19
+ // usually `.cmd`/`.ps1` wrappers that spawn() cannot find without a shell).
20
+ //
21
+ // Exit 0 on success (project landed at <dest>).
22
+ // Exit 1 if `sf template generate project` fails, the generated project has
23
+ // no sfdx-project.json, or the flatten didn't result in a valid project root.
24
+
25
+ import fs from "node:fs";
26
+ import path from "node:path";
27
+ import { spawnSync } from "node:child_process";
28
+ import { flattenProject } from "./flatten-project.mjs";
29
+
30
+ const [name, dest, template] = process.argv.slice(2);
31
+
32
+ function fail(message) {
33
+ console.error(message);
34
+ process.exit(1);
35
+ }
36
+
37
+ if (!name || !dest || !template) {
38
+ fail("usage: node <skill_dir>/scripts/generate-project.mjs <name> <dest> <template>");
39
+ }
40
+
41
+ fs.mkdirSync(dest, { recursive: true });
42
+
43
+ const result = spawnSync(
44
+ "sf",
45
+ ["template", "generate", "project", "--name", name, "--template", template, "--output-dir", dest],
46
+ { stdio: "inherit", shell: true },
47
+ );
48
+
49
+ if (result.error) {
50
+ fail(`ERROR: failed to run "sf template generate project": ${result.error.message}`);
51
+ }
52
+ if (result.status !== 0) {
53
+ process.exit(result.status ?? 1);
54
+ }
55
+
56
+ const srcDir = path.join(dest, name);
57
+
58
+ if (!fs.existsSync(path.join(srcDir, "sfdx-project.json"))) {
59
+ fail(`ERROR: generated project has no sfdx-project.json: ${srcDir}`);
60
+ }
61
+
62
+ try {
63
+ flattenProject(srcDir, dest);
64
+ } catch (err) {
65
+ fail(`ERROR: ${err.message}`);
66
+ }
67
+
68
+ // Remove the now-empty (or leftover) generated subfolder.
69
+ fs.rmSync(srcDir, { recursive: true, force: true });
70
+
71
+ const destProjectFile = path.join(dest, "sfdx-project.json");
72
+ if (!fs.existsSync(destProjectFile)) {
73
+ fail(`FAILED: sfdx-project.json not found at ${destProjectFile} — flatten did not run correctly`);
74
+ }
75
+
76
+ console.log(`OK: project root landed at ${dest} (${destProjectFile})`);
77
+ process.exit(0);
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ // Install dependencies for a generated UI bundle starter project: the
3
+ // project root `package.json`, plus each `package.json` under
4
+ // force-app/main/default/uiBundles/*/ (the toolchain the preview server
5
+ // loads). Then verify node_modules landed everywhere it was installed.
6
+ //
7
+ // Usage: node <skill_dir>/scripts/install-deps.mjs <dest>
8
+ // <dest> — the project root (same value passed to generate-project.mjs)
9
+ //
10
+ // Always invoke via `node` (never as a bare executable, and never wrap this
11
+ // in a bash/cmd/PowerShell script) so this works on Windows cmd/PowerShell
12
+ // as well as macOS/Linux/Git Bash. Runs `npm install` via spawnSync with an
13
+ // explicit `cwd` (no `cd &&` subshell chaining) and shell:true so the
14
+ // platform's own shell resolves the `npm` CLI shim correctly.
15
+ //
16
+ // Exit 0 if every install succeeded (or there was nothing to install).
17
+ // Exit 1 with a summary of which installs failed — surface it, don't hand
18
+ // off a half-installed project.
19
+
20
+ import fs from "node:fs";
21
+ import path from "node:path";
22
+ import { spawnSync } from "node:child_process";
23
+
24
+ const [dest] = process.argv.slice(2);
25
+
26
+ if (!dest) {
27
+ console.error("usage: node <skill_dir>/scripts/install-deps.mjs <dest>");
28
+ process.exit(1);
29
+ }
30
+
31
+ function npmInstall(cwd) {
32
+ console.log(`Running "npm install" in ${cwd} ...`);
33
+ const result = spawnSync("npm", ["install"], { cwd, stdio: "inherit", shell: true });
34
+ if (result.error) {
35
+ return `${cwd}: ${result.error.message}`;
36
+ }
37
+ if (result.status !== 0) {
38
+ return `${cwd}: npm install exited with code ${result.status}`;
39
+ }
40
+ return null;
41
+ }
42
+
43
+ const failures = [];
44
+
45
+ // 1. Project root.
46
+ if (fs.existsSync(path.join(dest, "package.json"))) {
47
+ const failure = npmInstall(dest);
48
+ if (failure) failures.push(failure);
49
+ } else {
50
+ console.error(`ERROR: no package.json found at project root: ${dest}`);
51
+ failures.push(`${dest}: missing package.json`);
52
+ }
53
+
54
+ // 2. Each UI bundle.
55
+ const bundlesDir = path.join(dest, "force-app", "main", "default", "uiBundles");
56
+ const bundleDirs = fs.existsSync(bundlesDir)
57
+ ? fs
58
+ .readdirSync(bundlesDir, { withFileTypes: true })
59
+ .filter((entry) => entry.isDirectory())
60
+ .map((entry) => path.join(bundlesDir, entry.name))
61
+ : [];
62
+
63
+ for (const bundleDir of bundleDirs) {
64
+ if (fs.existsSync(path.join(bundleDir, "package.json"))) {
65
+ const failure = npmInstall(bundleDir);
66
+ if (failure) failures.push(failure);
67
+ }
68
+ }
69
+
70
+ // 3. Verify node_modules landed everywhere it was installed.
71
+ console.log("\nVerifying installs:");
72
+ let allInstalled = true;
73
+ if (fs.existsSync(path.join(dest, "package.json"))) {
74
+ const ok = fs.existsSync(path.join(dest, "node_modules"));
75
+ console.log(` ${ok ? "OK" : "MISSING"}: ${path.join(dest, "node_modules")}`);
76
+ allInstalled = allInstalled && ok;
77
+ }
78
+ for (const bundleDir of bundleDirs) {
79
+ if (fs.existsSync(path.join(bundleDir, "package.json"))) {
80
+ const ok = fs.existsSync(path.join(bundleDir, "node_modules"));
81
+ console.log(` ${ok ? "OK" : "MISSING"}: ${path.join(bundleDir, "node_modules")}`);
82
+ allInstalled = allInstalled && ok;
83
+ }
84
+ }
85
+
86
+ if (failures.length > 0 || !allInstalled) {
87
+ console.error("\nFAILED: one or more installs did not complete:");
88
+ for (const failure of failures) {
89
+ console.error(` - ${failure}`);
90
+ }
91
+ process.exit(1);
92
+ }
93
+
94
+ console.log("\nOK: all dependencies installed.");
95
+ process.exit(0);
@@ -38,8 +38,8 @@ Use this skill when the user needs **OAuth app configuration** in Salesforce: Co
38
38
 
39
39
  **Out of scope — delegate elsewhere:**
40
40
  - Configuring Named Credentials or runtime callouts → [integration-connectivity-generate](../integration-connectivity-generate/SKILL.md)
41
- - Deploying metadata to orgs → [platform-metadata-deploy](../platform-metadata-deploy/SKILL.md)
42
- - Writing Apex token-handling code → [platform-apex-generate](../platform-apex-generate/SKILL.md)
41
+ - Deploying metadata to orgs → the `platform-metadata-deploy` skill
42
+ - Writing Apex token-handling code → the `platform-apex-generate` skill
43
43
 
44
44
  ---
45
45
 
@@ -206,8 +206,8 @@ Score: <x>/120
206
206
  | Need | Delegate to | Reason |
207
207
  |---|---|---|
208
208
  | Named Credential / callout runtime config | [integration-connectivity-generate](../integration-connectivity-generate/SKILL.md) | runtime integration setup |
209
- | Deploy app metadata | [platform-metadata-deploy](../platform-metadata-deploy/SKILL.md) | org validation and deployment |
210
- | Apex token or refresh handling | [platform-apex-generate](../platform-apex-generate/SKILL.md) | implementation logic |
209
+ | Deploy app metadata | `platform-metadata-deploy` skill | org validation and deployment |
210
+ | Apex token or refresh handling | `platform-apex-generate` skill | implementation logic |
211
211
 
212
212
  ---
213
213
 
@@ -237,4 +237,4 @@ Score: <x>/120
237
237
  | `references/security-checklist.md` | Step 4 — full 120-point security scoring checklist |
238
238
  | `references/testing-validation-guide.md` | Step 5 — pre-deployment validation and testing guide |
239
239
  | `references/migration-guide.md` | When migrating from Connected App to ECA patterns |
240
- | `references/example-usage.md` | Full end-to-end examples for common OAuth scenarios |
240
+ | `references/example-usage.md` | Full end-to-end examples for common OAuth scenarios |
@@ -39,9 +39,9 @@ Use `integration-connectivity-generate` when the work involves:
39
39
 
40
40
  Delegate elsewhere when the user is:
41
41
  - configuring the OAuth app itself → [integration-connectivity-connected-app-configure](../integration-connectivity-connected-app-configure/SKILL.md)
42
- - writing Apex-only business logic → [platform-apex-generate](../platform-apex-generate/SKILL.md)
43
- - deploying metadata → [platform-metadata-deploy](../platform-metadata-deploy/SKILL.md)
44
- - importing/exporting data → [platform-data-manage](../platform-data-manage/SKILL.md)
42
+ - writing Apex-only business logic → the `platform-apex-generate` skill
43
+ - deploying metadata → the `platform-metadata-deploy` skill
44
+ - importing/exporting data → the `platform-data-manage` skill
45
45
 
46
46
  ---
47
47
 
@@ -93,9 +93,9 @@ Check:
93
93
 
94
94
  ### 5. Hand off deployment or implementation details
95
95
  Use:
96
- - [platform-metadata-deploy](../platform-metadata-deploy/SKILL.md) for deployment
97
- - [platform-apex-generate](../platform-apex-generate/SKILL.md) for deeper service / retry code
98
- - [automation-flow-generate](../automation-flow-generate/SKILL.md) for declarative HTTP callout orchestration
96
+ - the `platform-metadata-deploy` skill for deployment
97
+ - the `platform-apex-generate` skill for deeper service / retry code
98
+ - the `automation-flow-generate` skill for declarative HTTP callout orchestration
99
99
 
100
100
  ---
101
101
 
@@ -142,10 +142,10 @@ Next step: <deploy, register, test, or implement>
142
142
  | Need | Delegate to | Reason |
143
143
  |---|---|---|
144
144
  | OAuth app setup | [integration-connectivity-connected-app-configure](../integration-connectivity-connected-app-configure/SKILL.md) | consumer key / cert / app config |
145
- | advanced callout service code | [platform-apex-generate](../platform-apex-generate/SKILL.md) | Apex implementation |
146
- | declarative HTTP callout / Flow wrapper | [automation-flow-generate](../automation-flow-generate/SKILL.md) | Flow orchestration |
147
- | deploy integration metadata | [platform-metadata-deploy](../platform-metadata-deploy/SKILL.md) | validation and rollout |
148
- | use integration from Agentforce | [agentforce-generate](../agentforce-generate/SKILL.md) | agent action composition |
145
+ | advanced callout service code | `platform-apex-generate` skill | Apex implementation |
146
+ | declarative HTTP callout / Flow wrapper | `automation-flow-generate` skill | Flow orchestration |
147
+ | deploy integration metadata | `platform-metadata-deploy` skill | validation and rollout |
148
+ | use integration from Agentforce | `agentforce-generate` skill | agent action composition |
149
149
 
150
150
  ---
151
151
 
@@ -190,14 +190,14 @@ try {
190
190
  // Create the credential (first-time setup)
191
191
  ConnectApi.NamedCredentials.createCredential(newCredentials);
192
192
  System.debug('✓ External Credential configured successfully!');
193
- System.debug('Principal: $PRINCIPAL_NAME');
193
+ System.debug('Principal: $PRINCIPAL_NAME_ESCAPED');
194
194
  } catch (Exception e) {
195
195
  // If already exists, try patching instead
196
196
  if (e.getMessage().contains('already exists')) {
197
197
  try {
198
198
  ConnectApi.NamedCredentials.patchCredential(newCredentials);
199
199
  System.debug('✓ External Credential updated successfully!');
200
- System.debug('Principal: $PRINCIPAL_NAME');
200
+ System.debug('Principal: $PRINCIPAL_NAME_ESCAPED');
201
201
  } catch (Exception e2) {
202
202
  System.debug('✗ Error updating credential: ' + e2.getMessage());
203
203
  throw e2;