@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.
- package/package.json +1 -1
- package/skills/commerce-b2b-open-code-components-integrate/SKILL.md +1 -1
- package/skills/commerce-b2b-open-code-components-replace/SKILL.md +16 -16
- package/skills/dx-devops-project-manage/SKILL.md +1 -1
- package/skills/experience-cms-content-generate/SKILL.md +1 -1
- package/skills/experience-cms-content-render/SKILL.md +3 -3
- package/skills/experience-cms-content-render/assets/angular/MediaRenderer.component.ts +35 -7
- package/skills/experience-cms-content-render/assets/angular/cms-item.service.ts +13 -4
- package/skills/experience-cms-content-render/assets/react/MediaRenderer.tsx +30 -8
- package/skills/experience-cms-content-render/assets/react/useCmsItem.ts +14 -4
- package/skills/experience-cms-content-render/assets/shared/cmsCore.types.ts +11 -1
- package/skills/experience-cms-content-render/assets/shared/externalRefs.ts +1 -1
- package/skills/experience-cms-content-render/assets/shared/mediaLabels.ts +62 -0
- package/skills/experience-cms-content-render/references/init-scaffold.md +13 -6
- package/skills/experience-content-media-stock-image-search/SKILL.md +0 -1
- package/skills/experience-lwc-design-generate/SKILL.md +1 -1
- package/skills/experience-lwc-legacy-migrate/SKILL.md +212 -0
- package/skills/experience-lwc-legacy-migrate/assets/lo20-host-page-template.html +192 -0
- package/skills/experience-lwc-legacy-migrate/references/aura-to-lwc-completeness-checklist.md +188 -0
- package/skills/experience-lwc-legacy-migrate/references/lightning-out-2-system-reference.md +495 -0
- package/skills/experience-lwc-legacy-migrate/references/lightning-out-beta-to-2-migration.md +824 -0
- package/skills/experience-lwc-legacy-migrate/scripts/convert-lo-names.py +94 -0
- package/skills/experience-lwc-legacy-migrate/scripts/validate-lo20-page.py +133 -0
- package/skills/experience-ui-bundle-project-generate/SKILL.md +14 -40
- package/skills/experience-ui-bundle-project-generate/scripts/flatten-project.mjs +33 -11
- package/skills/experience-ui-bundle-project-generate/scripts/generate-project.mjs +77 -0
- package/skills/experience-ui-bundle-project-generate/scripts/install-deps.mjs +95 -0
- package/skills/integration-connectivity-connected-app-configure/SKILL.md +5 -5
- package/skills/integration-connectivity-generate/SKILL.md +10 -10
- package/skills/integration-connectivity-generate/scripts/configure-named-credential.sh +2 -2
- package/skills/platform-apex-test-generate/SKILL.md +1 -1
- package/skills/platform-apex-test-run/SKILL.md +1 -1
- package/skills/platform-custom-field-generate/SKILL.md +1 -1
- package/skills/platform-custom-lightning-type-generate/SKILL.md +1 -1
- package/skills/platform-custom-metadata-type-generate/SKILL.md +1 -1
- package/skills/platform-custom-object-generate/SKILL.md +1 -1
- package/skills/platform-custom-report-type-generate/SKILL.md +1 -1
- package/skills/platform-custom-setting-generate/SKILL.md +1 -1
- package/skills/platform-data-and-tooling-api-context-get/SKILL.md +1 -1
- package/skills/platform-data-manage/SKILL.md +10 -10
- package/skills/platform-flexipage-generate/SKILL.md +1 -1
- package/skills/platform-lightning-app-coordinate/SKILL.md +1 -1
- package/skills/platform-metadata-api-context-get/SKILL.md +1 -1
- package/skills/platform-metadata-retrieve/SKILL.md +1 -1
- package/skills/platform-permission-set-generate/SKILL.md +1 -1
- package/skills/platform-report-generate/SKILL.md +1 -1
- package/skills/platform-salesforce-connect-adapter-generate/SKILL.md +4 -4
- package/skills/platform-sharing-owd-configure/SKILL.md +1 -1
- package/skills/platform-sharing-rules-generate/SKILL.md +1 -1
- package/skills/platform-soql-query/SKILL.md +1 -1
- package/skills/platform-value-set-generate/SKILL.md +1 -1
- package/skills/service-agentforce-human-escalation-configure/SKILL.md +4 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/tests/_bootstrap.py +26 -6
- package/skills/service-agentforce-human-escalation-configure/scripts/tests/test_escalation_contracts.py +5 -4
- package/skills/service-de-waba-integrate/SKILL.md +2 -1
- package/skills/service-digital-engagement-deployment-configure/SKILL.md +2 -0
- package/skills/service-digital-engagement-deployment-configure/scripts/check-api-version.sh +29 -0
- package/skills/service-email-to-case-configure/SKILL.md +1 -1
- package/skills/service-omni-channel-setup-coordinate/SKILL.md +1 -0
- package/skills/service-omni-command-center-analyze/SKILL.md +1 -0
- package/skills/service-omni-command-center-configure/SKILL.md +92 -0
- package/skills/service-omni-command-center-configure/references/api-notes.md +50 -0
- package/skills/service-omni-command-center-configure/scripts/configure-and-report.sh +257 -0
- package/skills/service-omni-command-center-configure/scripts/settings_document.py +97 -0
- 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);
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
55
|
+
node "<SKILL_DIR>/scripts/generate-project.mjs" "<name>" "<dest>" "<template>"
|
|
75
56
|
```
|
|
76
57
|
|
|
77
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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 →
|
|
42
|
-
- Writing Apex token-handling code →
|
|
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 |
|
|
210
|
-
| Apex token or refresh handling |
|
|
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 →
|
|
43
|
-
- deploying metadata →
|
|
44
|
-
- importing/exporting data →
|
|
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
|
-
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
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 |
|
|
146
|
-
| declarative HTTP callout / Flow wrapper |
|
|
147
|
-
| deploy integration metadata |
|
|
148
|
-
| use integration from Agentforce |
|
|
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: $
|
|
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: $
|
|
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;
|