@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,257 @@
1
+ #!/usr/bin/env bash
2
+ # Configure Command Center for Service V2 through a preserved OmniChannel Settings document.
3
+
4
+ set -euo pipefail
5
+
6
+ usage() {
7
+ echo '{"status":"blocked","reason_code":"invalid_arguments","blocking_issue":"Usage: bash configure-and-report.sh <plan|run> <org-alias> [--apply] [--conversation-monitoring true|false] [--agent-sneak-peek true|false] [--customer-sneak-peek true|false] [--whisper-messaging true|false] [--queues-and-skills true|false]"}' >&2
8
+ exit 1
9
+ }
10
+
11
+ [ "$#" -ge 2 ] || usage
12
+
13
+ MODE="$1"
14
+ ORG="$2"
15
+ shift 2
16
+
17
+ case "$MODE" in
18
+ plan|run) ;;
19
+ *) usage ;;
20
+ esac
21
+
22
+ APPLY=false
23
+ REQUESTED_JSON='{"enableCommandCenterForServiceV2":true}'
24
+ ASSIGNMENTS=("enableCommandCenterForServiceV2=true")
25
+
26
+ add_setting() {
27
+ local metadata_name="$1"
28
+ local value="$2"
29
+ if [ "$value" != "true" ] && [ "$value" != "false" ]; then
30
+ jq -n --arg name "$metadata_name" --arg value "$value" \
31
+ '{status:"blocked",reason_code:"invalid_arguments",blocking_issue:("Expected true or false for " + $name + ", received: " + $value)}' >&2
32
+ exit 1
33
+ fi
34
+ REQUESTED_JSON=$(jq -c --arg name "$metadata_name" --argjson value "$value" '. + {($name):$value}' <<<"$REQUESTED_JSON")
35
+ ASSIGNMENTS+=("$metadata_name=$value")
36
+ }
37
+
38
+ while [ "$#" -gt 0 ]; do
39
+ case "$1" in
40
+ --apply)
41
+ APPLY=true
42
+ shift
43
+ ;;
44
+ --conversation-monitoring|--agent-sneak-peek|--customer-sneak-peek|--whisper-messaging|--queues-and-skills)
45
+ [ "$#" -ge 2 ] || usage
46
+ case "$1" in
47
+ --conversation-monitoring) FIELD="enableConversationMonitoring" ;;
48
+ --agent-sneak-peek) FIELD="enableAgentSneakPeek" ;;
49
+ --customer-sneak-peek) FIELD="enableClientSneakPeek" ;;
50
+ --whisper-messaging) FIELD="enableWhisperMessaging" ;;
51
+ --queues-and-skills) FIELD="enableSkillsAndQueueActions" ;;
52
+ esac
53
+ add_setting "$FIELD" "$2"
54
+ shift 2
55
+ ;;
56
+ *) usage ;;
57
+ esac
58
+ done
59
+
60
+ if [ "$MODE" = "plan" ] && [ "$APPLY" = "true" ]; then
61
+ jq -n '{status:"blocked",reason_code:"invalid_arguments",blocking_issue:"--apply is not valid in plan mode."}' >&2
62
+ exit 1
63
+ fi
64
+
65
+ if [ "$MODE" = "run" ] && [ "$APPLY" != "true" ]; then
66
+ jq -n --argjson requested "$REQUESTED_JSON" \
67
+ '{status:"blocked",reason_code:"apply_required",requested:$requested,safe_to_write:null,deploy_id:null,blocking_issue:"run mode requires explicit --apply consent; no org changes were made."}'
68
+ exit 1
69
+ fi
70
+
71
+ for dependency in sf jq python3; do
72
+ if ! command -v "$dependency" >/dev/null 2>&1; then
73
+ jq -n --arg dependency "$dependency" \
74
+ '{status:"blocked",reason_code:"missing_dependency",blocking_issue:("Required command is unavailable: " + $dependency)}' >&2
75
+ exit 1
76
+ fi
77
+ done
78
+
79
+ if ! sf org display --target-org "$ORG" --json >/dev/null 2>&1; then
80
+ jq -n --arg org "$ORG" \
81
+ '{status:"blocked",reason_code:"org_not_authenticated",blocking_issue:("Org alias is not authenticated: " + $org)}' >&2
82
+ exit 1
83
+ fi
84
+
85
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
86
+ DOCUMENT_TOOL="$SCRIPT_DIR/settings_document.py"
87
+ WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/omni-command-center-configure.XXXXXX")"
88
+ trap 'rm -rf "$WORK_DIR"' EXIT
89
+
90
+ create_project() {
91
+ local directory="$1"
92
+ mkdir -p "$directory/force-app/main/default"
93
+ printf '%s\n' '{"packageDirectories":[{"path":"force-app","default":true}],"sourceApiVersion":"66.0"}' > "$directory/sfdx-project.json"
94
+ }
95
+
96
+ retrieve_settings() {
97
+ local directory="$1"
98
+ create_project "$directory"
99
+ (cd "$directory" && sf project retrieve start --target-org "$ORG" --metadata "Settings:OmniChannel" --json >/dev/null 2>&1)
100
+ }
101
+
102
+ settings_path() {
103
+ printf '%s/force-app/main/default/settings/OmniChannel.settings-meta.xml' "$1"
104
+ }
105
+
106
+ if ! retrieve_settings "$WORK_DIR/before"; then
107
+ jq -n --argjson requested "$REQUESTED_JSON" \
108
+ '{status:"blocked",reason_code:"retrieve_failed",requested:$requested,blocking_issue:"Could not retrieve Settings:OmniChannel. Verify org access and retry."}'
109
+ exit 1
110
+ fi
111
+
112
+ BEFORE_XML="$(settings_path "$WORK_DIR/before")"
113
+ if [ ! -f "$BEFORE_XML" ]; then
114
+ jq -n --argjson requested "$REQUESTED_JSON" \
115
+ '{status:"blocked",reason_code:"retrieve_failed",requested:$requested,blocking_issue:"The retrieve completed without an OmniChannel settings document."}'
116
+ exit 1
117
+ fi
118
+
119
+ if ! BEFORE_JSON=$(python3 "$DOCUMENT_TOOL" inspect "$BEFORE_XML" 2>/dev/null); then
120
+ jq -n --argjson requested "$REQUESTED_JSON" \
121
+ '{status:"blocked",reason_code:"invalid_settings_document",requested:$requested,blocking_issue:"The retrieved OmniChannel settings document could not be parsed."}'
122
+ exit 1
123
+ fi
124
+
125
+ API_PRESENT=$(jq -r '.settings.enableCommandCenterForServiceV2.present' <<<"$BEFORE_JSON")
126
+ if [ "$API_PRESENT" != "true" ]; then
127
+ jq -n --argjson requested "$REQUESTED_JSON" --argjson before "$BEFORE_JSON" \
128
+ '{status:"blocked",reason_code:"platform_api_unavailable",requested:$requested,before:$before,after:null,safe_to_write:null,deploy_id:null,verification:null,manual_actions:[{id:"WAIT_FOR_W_24039822",title:"Use a target release containing the W-24039822 Metadata API contract (Core PR #16874)."}],blocking_issue:"The target org does not expose enableCommandCenterForServiceV2 in Settings:OmniChannel; no deployment was attempted."}'
129
+ exit 1
130
+ fi
131
+
132
+ values_match() {
133
+ local snapshot="$1"
134
+ jq -ne --argjson requested "$REQUESTED_JSON" --argjson snapshot "$snapshot" '
135
+ all($requested | to_entries[];
136
+ (.key as $name | .value as $wanted |
137
+ $snapshot.settings[$name].present == true and $snapshot.settings[$name].value == $wanted)
138
+ )
139
+ ' >/dev/null
140
+ }
141
+
142
+ query_verification() {
143
+ local seed_json tab_json seed tab
144
+ seed_json=$(sf data query --target-org "$ORG" --use-tooling-api --json \
145
+ --query "SELECT Id FROM FlexiPage WHERE DeveloperName='CommandCenterForServiceV2_L'" 2>/dev/null || true)
146
+ if [ "$(jq -r '.status // 1' <<<"$seed_json" 2>/dev/null)" = "0" ]; then
147
+ if [ "$(jq -r '.result.totalSize // 0' <<<"$seed_json")" -gt 0 ]; then seed=true; else seed=false; fi
148
+ else
149
+ seed=unknown
150
+ fi
151
+
152
+ tab_json=$(sf data query --target-org "$ORG" --json \
153
+ --query "SELECT Name FROM TabDefinition WHERE Name='standard-commandcenterforservicev2' LIMIT 1" 2>/dev/null || true)
154
+ if [ "$(jq -r '.status // 1' <<<"$tab_json" 2>/dev/null)" = "0" ]; then
155
+ if [ "$(jq -r '.result.totalSize // 0' <<<"$tab_json")" -gt 0 ]; then tab=true; else tab=false; fi
156
+ else
157
+ tab=unknown
158
+ fi
159
+
160
+ jq -n --arg seed "$seed" --arg tab "$tab" \
161
+ '{seed_flexipage_present:$seed,v2_tab_present:$tab,complete:($seed == "true" and $tab == "true")}'
162
+ }
163
+
164
+ BEFORE_MATCH=false
165
+ if values_match "$BEFORE_JSON"; then BEFORE_MATCH=true; fi
166
+
167
+ if [ "$BEFORE_MATCH" = "true" ]; then
168
+ VERIFY_JSON=$(query_verification)
169
+ if [ "$(jq -r '.complete' <<<"$VERIFY_JSON")" != "true" ]; then
170
+ jq -n --argjson requested "$REQUESTED_JSON" --argjson before "$BEFORE_JSON" --argjson verification "$VERIFY_JSON" \
171
+ '{status:"blocked",reason_code:"seed_incomplete",requested:$requested,before:$before,after:$before,safe_to_write:null,deploy_id:null,verification:$verification,manual_actions:[{id:"REPAIR_V2_SEED",title:"The V2 preference is already enabled but its page or tab cannot be proven. Diagnose the platform seed before retrying; this skill will not force an OFF-to-ON cycle."}],blocking_issue:"Command Center V2 is enabled but its seed artifacts are missing or unreadable."}'
172
+ exit 1
173
+ fi
174
+
175
+ jq -n --argjson requested "$REQUESTED_JSON" --argjson before "$BEFORE_JSON" --argjson verification "$VERIFY_JSON" \
176
+ '{status:"reused",reason_code:"already_configured",requested:$requested,before:$before,after:$before,safe_to_write:null,deploy_id:null,verification:$verification,manual_actions:[{id:"ASSIGN_V2_PERMISSION",title:"Assign CommandCenterForServiceUser to each intended supervisor, then verify with service-omni-command-center-analyze."}],blocking_issue:null}'
177
+ exit 0
178
+ fi
179
+
180
+ if [ "$MODE" = "plan" ]; then
181
+ jq -n --argjson requested "$REQUESTED_JSON" --argjson before "$BEFORE_JSON" \
182
+ '{status:"action_needed",reason_code:"changes_required",requested:$requested,before:$before,after:null,safe_to_write:null,deploy_id:null,verification:null,manual_actions:[],blocking_issue:null}'
183
+ exit 0
184
+ fi
185
+
186
+ ORG_GUARD_JSON=$(sf data query --target-org "$ORG" --json \
187
+ --query "SELECT IsSandbox, TrialExpirationDate, OrganizationType FROM Organization LIMIT 1" 2>/dev/null || true)
188
+ if [ "$(jq -r '.status // 1' <<<"$ORG_GUARD_JSON" 2>/dev/null)" != "0" ]; then
189
+ jq -n --argjson requested "$REQUESTED_JSON" --argjson before "$BEFORE_JSON" \
190
+ '{status:"blocked",reason_code:"guard_query_failed",requested:$requested,before:$before,safe_to_write:null,deploy_id:null,blocking_issue:"Could not evaluate the non-production safe_to_write guard."}'
191
+ exit 1
192
+ fi
193
+
194
+ IS_SANDBOX=$(jq -r '.result.records[0].IsSandbox' <<<"$ORG_GUARD_JSON")
195
+ TRIAL_EXP=$(jq -r '.result.records[0].TrialExpirationDate // "null"' <<<"$ORG_GUARD_JSON")
196
+ ORG_TYPE=$(jq -r '.result.records[0].OrganizationType // ""' <<<"$ORG_GUARD_JSON")
197
+ SAFE_TO_WRITE=false
198
+ if [ "$IS_SANDBOX" = "true" ] || [ "$TRIAL_EXP" != "null" ] || [ "$ORG_TYPE" = "Developer Edition" ] || [ "$ORG_TYPE" = "Base Edition" ]; then
199
+ SAFE_TO_WRITE=true
200
+ fi
201
+
202
+ if [ "$SAFE_TO_WRITE" != "true" ]; then
203
+ jq -n --argjson requested "$REQUESTED_JSON" --argjson before "$BEFORE_JSON" \
204
+ '{status:"blocked",reason_code:"unsafe_target",requested:$requested,before:$before,safe_to_write:false,deploy_id:null,blocking_issue:"Refusing to configure Command Center V2 in a production customer org."}'
205
+ exit 1
206
+ fi
207
+
208
+ create_project "$WORK_DIR/deploy"
209
+ DEPLOY_XML="$(settings_path "$WORK_DIR/deploy")"
210
+ if ! python3 "$DOCUMENT_TOOL" render "$BEFORE_XML" "$DEPLOY_XML" "${ASSIGNMENTS[@]}" >/dev/null; then
211
+ jq -n --argjson requested "$REQUESTED_JSON" --argjson before "$BEFORE_JSON" \
212
+ '{status:"blocked",reason_code:"render_failed",requested:$requested,before:$before,safe_to_write:true,deploy_id:null,blocking_issue:"Could not construct the preserved OmniChannel settings document."}'
213
+ exit 1
214
+ fi
215
+
216
+ DEPLOY_JSON=$(cd "$WORK_DIR/deploy" && sf project deploy start --target-org "$ORG" --source-dir "$DEPLOY_XML" --json 2>/dev/null || true)
217
+ DEPLOY_SUCCESS=$(jq -r '.result.success // false' <<<"$DEPLOY_JSON" 2>/dev/null)
218
+ DEPLOY_STATUS=$(jq -r '.result.status // ""' <<<"$DEPLOY_JSON" 2>/dev/null)
219
+ DEPLOY_ID=$(jq -r '.result.id // ""' <<<"$DEPLOY_JSON" 2>/dev/null)
220
+ if [ "$DEPLOY_SUCCESS" != "true" ] || [ "$DEPLOY_STATUS" = "SucceededPartial" ]; then
221
+ DEPLOY_ERROR=$(jq -r '.result.details.componentFailures // [] | if type == "array" then map(.problem) | join("; ") else .problem // "" end' <<<"$DEPLOY_JSON" 2>/dev/null)
222
+ if [ -z "$DEPLOY_ERROR" ] || [ "$DEPLOY_ERROR" = "null" ]; then
223
+ DEPLOY_ERROR=$(jq -r '.message // .result.errorMessage // "Unknown deploy error"' <<<"$DEPLOY_JSON" 2>/dev/null)
224
+ fi
225
+ jq -n --argjson requested "$REQUESTED_JSON" --argjson before "$BEFORE_JSON" --arg id "$DEPLOY_ID" --arg error "$DEPLOY_ERROR" \
226
+ '{status:"blocked",reason_code:"deploy_failed",requested:$requested,before:$before,after:null,safe_to_write:true,deploy_id:(if $id == "" then null else $id end),verification:null,blocking_issue:("OmniChannel settings deployment failed: " + $error)}'
227
+ exit 1
228
+ fi
229
+
230
+ if ! retrieve_settings "$WORK_DIR/after"; then
231
+ jq -n --argjson requested "$REQUESTED_JSON" --argjson before "$BEFORE_JSON" --arg id "$DEPLOY_ID" \
232
+ '{status:"blocked",reason_code:"verification_failed",requested:$requested,before:$before,after:null,safe_to_write:true,deploy_id:(if $id == "" then null else $id end),verification:null,blocking_issue:"Deployment succeeded, but post-deploy Settings:OmniChannel retrieval failed."}'
233
+ exit 1
234
+ fi
235
+
236
+ AFTER_XML="$(settings_path "$WORK_DIR/after")"
237
+ if [ ! -f "$AFTER_XML" ] || ! AFTER_JSON=$(python3 "$DOCUMENT_TOOL" inspect "$AFTER_XML" 2>/dev/null); then
238
+ jq -n --argjson requested "$REQUESTED_JSON" --argjson before "$BEFORE_JSON" --arg id "$DEPLOY_ID" \
239
+ '{status:"blocked",reason_code:"verification_failed",requested:$requested,before:$before,after:null,safe_to_write:true,deploy_id:(if $id == "" then null else $id end),verification:null,blocking_issue:"Deployment succeeded, but the post-deploy settings document could not be inspected."}'
240
+ exit 1
241
+ fi
242
+
243
+ if ! values_match "$AFTER_JSON"; then
244
+ jq -n --argjson requested "$REQUESTED_JSON" --argjson before "$BEFORE_JSON" --argjson after "$AFTER_JSON" --arg id "$DEPLOY_ID" \
245
+ '{status:"blocked",reason_code:"verification_failed",requested:$requested,before:$before,after:$after,safe_to_write:true,deploy_id:(if $id == "" then null else $id end),verification:null,blocking_issue:"Deployment reported success, but one or more requested settings did not persist."}'
246
+ exit 1
247
+ fi
248
+
249
+ VERIFY_JSON=$(query_verification)
250
+ if [ "$(jq -r '.complete' <<<"$VERIFY_JSON")" != "true" ]; then
251
+ jq -n --argjson requested "$REQUESTED_JSON" --argjson before "$BEFORE_JSON" --argjson after "$AFTER_JSON" --argjson verification "$VERIFY_JSON" --arg id "$DEPLOY_ID" \
252
+ '{status:"blocked",reason_code:"seed_incomplete",requested:$requested,before:$before,after:$after,safe_to_write:true,deploy_id:(if $id == "" then null else $id end),verification:$verification,manual_actions:[{id:"INSPECT_V2_SEED",title:"The settings persisted, but the V2 page and tab were not both observable. Inspect the platform seed hook before claiming readiness."}],blocking_issue:"Command Center V2 settings persisted, but seed verification is incomplete."}'
253
+ exit 1
254
+ fi
255
+
256
+ jq -n --argjson requested "$REQUESTED_JSON" --argjson before "$BEFORE_JSON" --argjson after "$AFTER_JSON" --argjson verification "$VERIFY_JSON" --arg id "$DEPLOY_ID" \
257
+ '{status:"configured",reason_code:"configured_and_verified",requested:$requested,before:$before,after:$after,safe_to_write:true,deploy_id:(if $id == "" then null else $id end),verification:$verification,manual_actions:[{id:"ASSIGN_V2_PERMISSION",title:"Assign CommandCenterForServiceUser to each intended supervisor, then verify with service-omni-command-center-analyze."}],blocking_issue:null}'
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env python3
2
+ """Inspect or safely mutate a retrieved OmniChannel Settings document."""
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+ import xml.etree.ElementTree as ET
8
+ from pathlib import Path
9
+ from typing import List, Optional, Tuple
10
+
11
+
12
+ METADATA_NS = "http://soap.sforce.com/2006/04/metadata"
13
+ FIELDS = (
14
+ "enableCommandCenterForServiceV2",
15
+ "enableConversationMonitoring",
16
+ "enableAgentSneakPeek",
17
+ "enableClientSneakPeek",
18
+ "enableWhisperMessaging",
19
+ "enableSkillsAndQueueActions",
20
+ )
21
+
22
+
23
+ def parse_document(path: Path) -> Tuple[ET.ElementTree, ET.Element]:
24
+ tree = ET.parse(path)
25
+ return tree, tree.getroot()
26
+
27
+
28
+ def find_element(root: ET.Element, name: str) -> Optional[ET.Element]:
29
+ return root.find(f"{{{METADATA_NS}}}{name}")
30
+
31
+
32
+ def inspect(path: Path) -> int:
33
+ _, root = parse_document(path)
34
+ settings = {}
35
+ for name in FIELDS:
36
+ element = find_element(root, name)
37
+ text = (element.text or "").strip().lower() if element is not None else ""
38
+ settings[name] = {
39
+ "present": element is not None,
40
+ "value": text == "true" if text in {"true", "false"} else None,
41
+ }
42
+ print(json.dumps({"settings": settings}, separators=(",", ":")))
43
+ return 0
44
+
45
+
46
+ def render(source: Path, destination: Path, assignments: List[str]) -> int:
47
+ tree, root = parse_document(source)
48
+ requested = {}
49
+ for assignment in assignments:
50
+ if "=" not in assignment:
51
+ raise ValueError(f"Invalid assignment: {assignment}")
52
+ name, value = assignment.split("=", 1)
53
+ if name not in FIELDS:
54
+ raise ValueError(f"Unsupported setting: {name}")
55
+ if value not in {"true", "false"}:
56
+ raise ValueError(f"Invalid boolean for {name}: {value}")
57
+ requested[name] = value
58
+
59
+ if "enableCommandCenterForServiceV2" not in requested:
60
+ raise ValueError("enableCommandCenterForServiceV2 must be requested")
61
+
62
+ for name, value in requested.items():
63
+ element = find_element(root, name)
64
+ if element is None:
65
+ element = ET.SubElement(root, f"{{{METADATA_NS}}}{name}")
66
+ element.text = value
67
+
68
+ ET.register_namespace("", METADATA_NS)
69
+ destination.parent.mkdir(parents=True, exist_ok=True)
70
+ tree.write(destination, encoding="UTF-8", xml_declaration=True)
71
+ return 0
72
+
73
+
74
+ def main() -> int:
75
+ parser = argparse.ArgumentParser()
76
+ subparsers = parser.add_subparsers(dest="operation", required=True)
77
+
78
+ inspect_parser = subparsers.add_parser("inspect")
79
+ inspect_parser.add_argument("path", type=Path)
80
+
81
+ render_parser = subparsers.add_parser("render")
82
+ render_parser.add_argument("source", type=Path)
83
+ render_parser.add_argument("destination", type=Path)
84
+ render_parser.add_argument("assignments", nargs="+")
85
+
86
+ args = parser.parse_args()
87
+ try:
88
+ if args.operation == "inspect":
89
+ return inspect(args.path)
90
+ return render(args.source, args.destination, args.assignments)
91
+ except (ET.ParseError, OSError, ValueError) as error:
92
+ print(json.dumps({"error": str(error)}, separators=(",", ":")), file=sys.stderr)
93
+ return 1
94
+
95
+
96
+ if __name__ == "__main__":
97
+ raise SystemExit(main())
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import subprocess
7
+ import tempfile
8
+ import unittest
9
+ from pathlib import Path
10
+
11
+
12
+ SKILLS_ROOT = Path(__file__).resolve().parents[3]
13
+ SCRIPT = SKILLS_ROOT / "service-omni-command-center-configure/scripts/configure-and-report.sh"
14
+ DOCUMENT_TOOL = SKILLS_ROOT / "service-omni-command-center-configure/scripts/settings_document.py"
15
+
16
+
17
+ FAKE_SF = r"""#!/usr/bin/env bash
18
+ set -euo pipefail
19
+ args="$*"
20
+ [ -z "${FAKE_CALL_LOG:-}" ] || printf '%s\n' "$args" >> "$FAKE_CALL_LOG"
21
+
22
+ if [ "${1:-} ${2:-}" = "org display" ]; then
23
+ echo '{"status":0,"result":{"username":"fake@example.invalid"}}'
24
+ exit 0
25
+ fi
26
+
27
+ if [ "${1:-} ${2:-}" = "project retrieve" ]; then
28
+ mkdir -p force-app/main/default/settings
29
+ target=force-app/main/default/settings/OmniChannel.settings-meta.xml
30
+ if [ -f "$FAKE_STATE_DIR/deployed.xml" ]; then
31
+ cp "$FAKE_STATE_DIR/deployed.xml" "$target"
32
+ else
33
+ {
34
+ echo '<?xml version="1.0" encoding="UTF-8"?>'
35
+ echo '<OmniChannelSettings xmlns="http://soap.sforce.com/2006/04/metadata">'
36
+ echo ' <enableOmniChannel>true</enableOmniChannel>'
37
+ echo ' <unrelatedSetting>true</unrelatedSetting>'
38
+ if [ "${FAKE_API_PRESENT:-1}" = "1" ]; then
39
+ echo " <enableCommandCenterForServiceV2>${FAKE_V2:-false}</enableCommandCenterForServiceV2>"
40
+ echo " <enableConversationMonitoring>${FAKE_CONVERSATION:-false}</enableConversationMonitoring>"
41
+ echo " <enableAgentSneakPeek>${FAKE_AGENT_PEEK:-false}</enableAgentSneakPeek>"
42
+ echo " <enableClientSneakPeek>${FAKE_CLIENT_PEEK:-false}</enableClientSneakPeek>"
43
+ echo " <enableWhisperMessaging>${FAKE_WHISPER:-false}</enableWhisperMessaging>"
44
+ echo " <enableSkillsAndQueueActions>${FAKE_QUEUE_ACTIONS:-false}</enableSkillsAndQueueActions>"
45
+ fi
46
+ echo '</OmniChannelSettings>'
47
+ } > "$target"
48
+ fi
49
+ echo '{"status":0,"result":{"success":true}}'
50
+ exit 0
51
+ fi
52
+
53
+ if [ "${1:-} ${2:-}" = "project deploy" ]; then
54
+ if [ "${FAKE_DEPLOY_FAIL:-0}" = "1" ]; then
55
+ echo '{"status":1,"message":"simulated failure","result":{"success":false,"status":"Failed"}}'
56
+ exit 1
57
+ fi
58
+ source_path=""
59
+ while [ "$#" -gt 0 ]; do
60
+ if [ "$1" = "--source-dir" ]; then source_path="$2"; break; fi
61
+ shift
62
+ done
63
+ cp "$source_path" "$FAKE_STATE_DIR/deployed.xml"
64
+ touch "$FAKE_STATE_DIR/deployed"
65
+ echo '{"status":0,"result":{"success":true,"status":"Succeeded","id":"0AfFakeDeploy"}}'
66
+ exit 0
67
+ fi
68
+
69
+ if [ "${1:-} ${2:-}" = "data query" ]; then
70
+ if printf '%s' "$args" | grep -q 'FROM Organization'; then
71
+ if [ "${FAKE_PRODUCTION:-0}" = "1" ]; then
72
+ echo '{"status":0,"result":{"records":[{"IsSandbox":false,"TrialExpirationDate":null,"OrganizationType":"Enterprise Edition"}]}}'
73
+ else
74
+ echo '{"status":0,"result":{"records":[{"IsSandbox":true,"TrialExpirationDate":null,"OrganizationType":"Developer Edition"}]}}'
75
+ fi
76
+ elif printf '%s' "$args" | grep -q 'FROM FlexiPage'; then
77
+ if { [ -f "$FAKE_STATE_DIR/deployed" ] && [ "${FAKE_SEED_AFTER_DEPLOY:-1}" = "1" ]; } || [ "${FAKE_SEED_PRESENT:-0}" = "1" ]; then
78
+ echo '{"status":0,"result":{"records":[{"Id":"0M0Fake"}],"totalSize":1}}'
79
+ else
80
+ echo '{"status":0,"result":{"records":[],"totalSize":0}}'
81
+ fi
82
+ elif printf '%s' "$args" | grep -q 'FROM TabDefinition'; then
83
+ if { [ -f "$FAKE_STATE_DIR/deployed" ] && [ "${FAKE_TAB_AFTER_DEPLOY:-1}" = "1" ]; } || [ "${FAKE_TAB_PRESENT:-0}" = "1" ]; then
84
+ echo '{"status":0,"result":{"records":[{"Name":"standard-commandcenterforservicev2"}],"totalSize":1}}'
85
+ else
86
+ echo '{"status":0,"result":{"records":[],"totalSize":0}}'
87
+ fi
88
+ else
89
+ echo '{"status":0,"result":{"records":[],"totalSize":0}}'
90
+ fi
91
+ exit 0
92
+ fi
93
+
94
+ echo '{"status":1,"message":"unexpected fake sf invocation"}'
95
+ exit 1
96
+ """
97
+
98
+
99
+ def run_script(*args: str, **environment: str):
100
+ with tempfile.TemporaryDirectory(prefix="command-center-configure-") as directory:
101
+ root = Path(directory)
102
+ sf = root / "sf"
103
+ sf.write_text(FAKE_SF)
104
+ sf.chmod(0o755)
105
+ state = root / "state"
106
+ state.mkdir()
107
+ call_log = root / "calls.log"
108
+ env = dict(os.environ)
109
+ env.update(environment)
110
+ env["FAKE_STATE_DIR"] = str(state)
111
+ env["FAKE_CALL_LOG"] = str(call_log)
112
+ env["PATH"] = str(root) + os.pathsep + env.get("PATH", "")
113
+ result = subprocess.run(
114
+ ["bash", str(SCRIPT), *args],
115
+ capture_output=True,
116
+ text=True,
117
+ env=env,
118
+ cwd=str(SKILLS_ROOT),
119
+ )
120
+ calls = call_log.read_text() if call_log.exists() else ""
121
+ deployed = (state / "deployed.xml").read_text() if (state / "deployed.xml").exists() else ""
122
+ return result, calls, deployed
123
+
124
+
125
+ class CommandCenterConfigureContracts(unittest.TestCase):
126
+ def test_script_and_python_syntax(self):
127
+ shell = subprocess.run(["bash", "-n", str(SCRIPT)], capture_output=True, text=True)
128
+ self.assertEqual(shell.returncode, 0, shell.stderr)
129
+ compile_result = subprocess.run(
130
+ ["python3", "-c", f"import ast, pathlib; ast.parse(pathlib.Path({str(DOCUMENT_TOOL)!r}).read_text())"],
131
+ capture_output=True,
132
+ text=True,
133
+ env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"},
134
+ )
135
+ self.assertEqual(compile_result.returncode, 0, compile_result.stderr)
136
+
137
+ def test_run_requires_apply_before_any_sf_call(self):
138
+ result, calls, _ = run_script("run", "test-org")
139
+ self.assertEqual(result.returncode, 1)
140
+ self.assertEqual(json.loads(result.stdout)["reason_code"], "apply_required")
141
+ self.assertEqual(calls, "")
142
+
143
+ def test_plan_reports_changes_without_deploying(self):
144
+ result, calls, _ = run_script("plan", "test-org")
145
+ self.assertEqual(result.returncode, 0, result.stderr)
146
+ self.assertEqual(json.loads(result.stdout)["status"], "action_needed")
147
+ self.assertNotIn("project deploy", calls)
148
+
149
+ def test_missing_platform_field_fails_before_deploy(self):
150
+ result, calls, _ = run_script("run", "test-org", "--apply", FAKE_API_PRESENT="0")
151
+ self.assertEqual(result.returncode, 1)
152
+ self.assertEqual(json.loads(result.stdout)["reason_code"], "platform_api_unavailable")
153
+ self.assertNotIn("project deploy", calls)
154
+
155
+ def test_production_org_is_refused(self):
156
+ result, calls, _ = run_script("run", "test-org", "--apply", FAKE_PRODUCTION="1")
157
+ self.assertEqual(result.returncode, 1)
158
+ self.assertEqual(json.loads(result.stdout)["reason_code"], "unsafe_target")
159
+ self.assertNotIn("project deploy", calls)
160
+
161
+ def test_run_preserves_unrelated_and_unspecified_settings(self):
162
+ result, calls, deployed = run_script(
163
+ "run",
164
+ "test-org",
165
+ "--apply",
166
+ "--agent-sneak-peek",
167
+ "true",
168
+ )
169
+ payload = json.loads(result.stdout)
170
+ self.assertEqual(result.returncode, 0, result.stderr)
171
+ self.assertEqual(payload["status"], "configured")
172
+ self.assertIn("project deploy", calls)
173
+ self.assertIn("<unrelatedSetting>true</unrelatedSetting>", deployed)
174
+ self.assertIn("<enableConversationMonitoring>false</enableConversationMonitoring>", deployed)
175
+ self.assertIn("<enableAgentSneakPeek>true</enableAgentSneakPeek>", deployed)
176
+ self.assertIn("<enableCommandCenterForServiceV2>true</enableCommandCenterForServiceV2>", deployed)
177
+
178
+ def test_already_ready_is_reused_without_deploy(self):
179
+ result, calls, _ = run_script(
180
+ "run",
181
+ "test-org",
182
+ "--apply",
183
+ FAKE_V2="true",
184
+ FAKE_SEED_PRESENT="1",
185
+ FAKE_TAB_PRESENT="1",
186
+ )
187
+ self.assertEqual(result.returncode, 0, result.stderr)
188
+ self.assertEqual(json.loads(result.stdout)["status"], "reused")
189
+ self.assertNotIn("project deploy", calls)
190
+
191
+ def test_persisted_settings_without_seed_are_blocked(self):
192
+ result, _, _ = run_script(
193
+ "run",
194
+ "test-org",
195
+ "--apply",
196
+ FAKE_SEED_AFTER_DEPLOY="0",
197
+ FAKE_TAB_AFTER_DEPLOY="0",
198
+ )
199
+ self.assertEqual(result.returncode, 1)
200
+ payload = json.loads(result.stdout)
201
+ self.assertEqual(payload["reason_code"], "seed_incomplete")
202
+ self.assertEqual(payload["after"]["settings"]["enableCommandCenterForServiceV2"]["value"], True)
203
+
204
+
205
+ if __name__ == "__main__":
206
+ unittest.main(verbosity=2)