@salesforce/afv-skills 1.31.0 → 1.32.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/agentforce-bot-upgrade/SKILL.md +217 -0
- package/skills/agentforce-bot-upgrade/references/extraction-blueprint.md +242 -0
- package/skills/agentforce-bot-upgrade/references/generate-agent-spec.md +128 -0
- package/skills/agentforce-bot-upgrade/references/handoff-output-format.md +208 -0
- package/skills/agentforce-bot-upgrade/references/input-contract.md +101 -0
- package/skills/agentforce-bot-upgrade/references/mapping-rules.md +113 -0
- package/skills/agentforce-bot-upgrade/references/planner-workflow-reference.md +78 -0
- package/skills/agentforce-bot-upgrade/references/post-conversion-enhancements-reference.md +67 -0
- package/skills/agentforce-bot-upgrade/references/quality-checklist.md +33 -0
- package/skills/agentforce-bot-upgrade/references/sf-cli-bot-reference.md +116 -0
- package/skills/dx-devops-work-item-manage/SKILL.md +270 -0
- package/skills/dx-devops-work-item-manage/examples/common-workflows.md +479 -0
- package/skills/dx-devops-work-item-manage/references/cli-commands.md +476 -0
- package/skills/dx-org-manage/SKILL.md +8 -5
- package/skills/dx-org-trial-expiration-check/SKILL.md +239 -0
- package/skills/dx-org-trial-expiration-check/scripts/check_expiration.sh +588 -0
- package/skills/experience-ui-bundle-app-coordinate/SKILL.md +12 -9
- package/skills/experience-ui-bundle-custom-app-generate/SKILL.md +29 -5
- package/skills/experience-ui-bundle-custom-app-generate/references/configure-metadata-custom-application.md +1 -1
- package/skills/experience-ui-bundle-custom-app-generate/scripts/resolve-uibundle-path.sh +30 -0
- package/skills/experience-ui-bundle-features-generate/SKILL.md +86 -11
- package/skills/experience-ui-bundle-features-generate/references/conflict-resolution-schema.json +20 -0
- package/skills/experience-ui-bundle-features-generate/scripts/verify-react-bundle.sh +14 -0
- package/skills/experience-ui-bundle-frontend-generate/SKILL.md +6 -1
- package/skills/experience-ui-bundle-frontend-generate/references/component.md +34 -1
- package/skills/experience-ui-bundle-frontend-generate/references/page.md +1 -1
- package/skills/experience-ui-bundle-project-generate/SKILL.md +111 -0
- package/skills/experience-ui-bundle-project-generate/scripts/flatten-project.mjs +33 -0
- package/skills/platform-custom-lightning-type-generate/SKILL.md +9 -14
- package/skills/platform-custom-lightning-type-generate/references/widget-rendition.md +27 -111
- package/skills/platform-lightning-type-widget-coordinate/SKILL.md +214 -0
- package/skills/platform-lightning-type-widget-coordinate/examples/existing-lightning-type-with-widget-prompt.md +80 -0
- package/skills/platform-lightning-type-widget-coordinate/examples/new-lightning-type-with-widget-prompt.md +75 -0
- package/skills/platform-lightning-type-widget-coordinate/references/build-plan-format.md +67 -0
- package/skills/platform-lightning-type-widget-coordinate/references/lightning-type-discovery.md +124 -0
- package/skills/platform-lightning-type-widget-coordinate/references/validation-gates.md +130 -0
- package/skills/platform-sharing-owd-configure/SKILL.md +142 -0
- package/skills/platform-sharing-owd-configure/examples/get_owd_output.md +38 -0
- package/skills/platform-sharing-owd-configure/examples/update_owd_output.md +44 -0
- package/skills/platform-sharing-owd-configure/references/access_levels.md +49 -0
- package/skills/platform-sharing-owd-configure/references/metadata_api_approach.md +69 -0
- package/skills/platform-sharing-rules-generate/SKILL.md +89 -19
- package/skills/platform-sharing-rules-generate/examples/test-cases.md +381 -0
- package/skills/platform-widget-generate/SKILL.md +241 -0
- package/skills/platform-widget-generate/examples/conditional.json +73 -0
- package/skills/platform-widget-generate/examples/list-with-foreach.json +66 -0
- package/skills/platform-widget-generate/examples/single-object.json +64 -0
- package/skills/platform-widget-generate/references/schema-from-lightning-type.md +76 -0
- package/skills/platform-widget-generate/references/widget-bundle-layout.md +116 -0
- package/skills/platform-widget-generate/references/widget-meta-directives.md +130 -0
- package/skills/sales-agentforce-pipeline-management-configure/SKILL.md +268 -0
- package/skills/sales-agentforce-pipeline-management-configure/assets/field-completion-template.genAiPromptTemplate-meta.xml +141 -0
- package/skills/sales-agentforce-pipeline-management-configure/assets/pipeline_management_flow.flow-meta.xml +548 -0
- package/skills/sales-agentforce-pipeline-management-configure/assets/sales_management_agent.agent +2938 -0
- package/skills/sales-agentforce-pipeline-management-configure/assets/sales_management_agent.bundle-meta.xml +4 -0
- package/skills/sales-agentforce-pipeline-management-configure/assets/sfdx-project.json +12 -0
- package/skills/sales-agentforce-pipeline-management-configure/assets/stage-descriptions/bant.txt +11 -0
- package/skills/sales-agentforce-pipeline-management-configure/assets/stage-descriptions/meddic.txt +11 -0
- package/skills/sales-agentforce-pipeline-management-configure/assets/stage-descriptions/spiced.txt +11 -0
- package/skills/sales-agentforce-pipeline-management-configure/examples/custom-prompt-instructions.md +317 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/admin-communication.md +355 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/agent-creation.md +383 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/auth-and-cli.md +146 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/automation-matrix.md +23 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/autonomous-updates.md +215 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/canonical-agent-path.md +75 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/data-sources.md +155 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/field-completion-prompt-template.md +120 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/field-completion-prompts.md +149 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/flags.md +13 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/flow-clone-from-template.md +447 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/metadata-inventory.md +330 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/opportunity-stages.md +246 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/repair-diagnostics.md +265 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/setup-order.md +263 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/soap-api-enablement.md +528 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/CHANGES.md +180 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/README.md +56 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/add-field-suggestion.sh +679 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/create-agent.sh +302 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/create-flow.sh +250 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/define-agent-access.sh +430 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/deploy-settings.sh +77 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/enable-deal-agent.sh +241 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/enable-prerequisites.sh +538 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/flow-debug-and-verify.sh +385 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/retrieve-settings.sh +131 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/setup-all.sh +2791 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/agent-bundle-publish.sh +189 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/agent-detection.sh +239 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/flow-builder.sh +238 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/psg.sh +48 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/soap.sh +140 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/stage-descriptions.sh +448 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/test-opp.sh +166 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/verify-all.sh +636 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/verify-prompt-generation.sh +346 -0
package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/agent-bundle-publish.sh
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Shared library for Sales Agent authoring bundle publish and activation
|
|
3
|
+
#
|
|
4
|
+
# Functions:
|
|
5
|
+
# publish_and_activate_agent <org-alias> <on-error-behavior> [output-style]
|
|
6
|
+
#
|
|
7
|
+
# Parameters:
|
|
8
|
+
# org-alias: Salesforce org alias
|
|
9
|
+
# on-error-behavior: "exit" (hard fail on error) or "return" (return non-zero)
|
|
10
|
+
# output-style: "plain" (default, plain echo) or "structured" (log_pass/log_info style)
|
|
11
|
+
#
|
|
12
|
+
# Returns:
|
|
13
|
+
# 0 on success
|
|
14
|
+
# 1 on failure
|
|
15
|
+
#
|
|
16
|
+
# Environment:
|
|
17
|
+
# SCRIPT_DIR must be set (parent script's directory)
|
|
18
|
+
|
|
19
|
+
# Source agent-detection helpers (pm_bot_latest_version, etc.)
|
|
20
|
+
# Use a local resolution that doesn't depend on the caller's SCRIPT_DIR.
|
|
21
|
+
# shellcheck source=./agent-detection.sh
|
|
22
|
+
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/agent-detection.sh"
|
|
23
|
+
|
|
24
|
+
publish_and_activate_agent() {
|
|
25
|
+
local ORG_ALIAS="$1"
|
|
26
|
+
local ON_ERROR="${2:-exit}"
|
|
27
|
+
local OUTPUT_STYLE="${3:-plain}"
|
|
28
|
+
|
|
29
|
+
# Clear exported state from any prior call to prevent stale values on failure
|
|
30
|
+
unset AGENT_VERSION
|
|
31
|
+
unset AGENT_API_NAME
|
|
32
|
+
|
|
33
|
+
local AGENT_BUNDLE="$SCRIPT_DIR/../assets/sales_management_agent.agent"
|
|
34
|
+
local BUNDLE_META="$SCRIPT_DIR/../assets/sales_management_agent.bundle-meta.xml"
|
|
35
|
+
|
|
36
|
+
# Output helper functions based on style
|
|
37
|
+
local msg_header msg_pass msg_error msg_info
|
|
38
|
+
if [[ "$OUTPUT_STYLE" == "structured" ]]; then
|
|
39
|
+
msg_header() { log_info "$1"; }
|
|
40
|
+
msg_pass() { log_pass "$1"; }
|
|
41
|
+
msg_error() { echo " ERROR: $1"; }
|
|
42
|
+
msg_info() { echo " $1"; }
|
|
43
|
+
else
|
|
44
|
+
msg_header() { echo "=== $1 ==="; }
|
|
45
|
+
msg_pass() { echo " ✓ $1"; }
|
|
46
|
+
msg_error() { echo " ERROR: $1"; }
|
|
47
|
+
msg_info() { echo " $1"; }
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Check for bundle assets
|
|
51
|
+
if [[ ! -f "$AGENT_BUNDLE" || ! -f "$BUNDLE_META" ]]; then
|
|
52
|
+
msg_error "Agent bundle assets not found at: $AGENT_BUNDLE"
|
|
53
|
+
if [[ "$ON_ERROR" == "exit" ]]; then
|
|
54
|
+
echo ""
|
|
55
|
+
echo "SETUP FAILED: A Pipeline Management BotDefinition is REQUIRED for user interaction."
|
|
56
|
+
exit 1
|
|
57
|
+
fi
|
|
58
|
+
return 1
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
msg_header "Creating Sales Agent via authoring bundle"
|
|
62
|
+
|
|
63
|
+
# Create temp directory for bundle deployment
|
|
64
|
+
local TEMP_DIR
|
|
65
|
+
TEMP_DIR=$(mktemp -d -t sales-agent-bundle.XXXXXX)
|
|
66
|
+
# Use EXIT trap so it fires even on explicit exit calls
|
|
67
|
+
trap 'rm -rf "$TEMP_DIR"' EXIT
|
|
68
|
+
|
|
69
|
+
# Create sfdx-project.json in temp dir
|
|
70
|
+
cat > "$TEMP_DIR/sfdx-project.json" << 'SFDX'
|
|
71
|
+
{"packageDirectories": [{"path": "force-app", "default": true}], "sourceApiVersion": "67.0"}
|
|
72
|
+
SFDX
|
|
73
|
+
|
|
74
|
+
# Copy authoring bundle to temp deployment directory
|
|
75
|
+
mkdir -p "$TEMP_DIR/force-app/main/default/aiAuthoringBundles/SalesAgent"
|
|
76
|
+
if ! cp "$AGENT_BUNDLE" "$TEMP_DIR/force-app/main/default/aiAuthoringBundles/SalesAgent/SalesAgent.agent"; then
|
|
77
|
+
msg_error "Failed to stage bundle agent file"
|
|
78
|
+
rm -rf "$TEMP_DIR"
|
|
79
|
+
trap - EXIT
|
|
80
|
+
if [[ "$ON_ERROR" == "exit" ]]; then
|
|
81
|
+
echo ""
|
|
82
|
+
echo "SETUP FAILED: A Pipeline Management BotDefinition is REQUIRED for user interaction."
|
|
83
|
+
exit 1
|
|
84
|
+
fi
|
|
85
|
+
return 1
|
|
86
|
+
fi
|
|
87
|
+
if ! cp "$BUNDLE_META" "$TEMP_DIR/force-app/main/default/aiAuthoringBundles/SalesAgent/SalesAgent.bundle-meta.xml"; then
|
|
88
|
+
msg_error "Failed to stage bundle metadata file"
|
|
89
|
+
rm -rf "$TEMP_DIR"
|
|
90
|
+
trap - EXIT
|
|
91
|
+
if [[ "$ON_ERROR" == "exit" ]]; then
|
|
92
|
+
echo ""
|
|
93
|
+
echo "SETUP FAILED: A Pipeline Management BotDefinition is REQUIRED for user interaction."
|
|
94
|
+
exit 1
|
|
95
|
+
fi
|
|
96
|
+
return 1
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# Publish authoring bundle from temp directory
|
|
100
|
+
msg_info "Publishing Sales Agent authoring bundle..."
|
|
101
|
+
local PUBLISH_RESULT
|
|
102
|
+
PUBLISH_RESULT=$(cd "$TEMP_DIR" && sf agent publish authoring-bundle --json --api-name SalesAgent --target-org "$ORG_ALIAS" 2>/dev/null || echo '{"status":1}')
|
|
103
|
+
|
|
104
|
+
if ! echo "$PUBLISH_RESULT" | jq -e '.status == 0' >/dev/null 2>&1; then
|
|
105
|
+
local PUBLISH_ERR
|
|
106
|
+
PUBLISH_ERR=$(echo "$PUBLISH_RESULT" | jq -r '.message // .result.message // .result.errors[0].message // "unknown"' 2>/dev/null || echo "unknown")
|
|
107
|
+
msg_error "Authoring bundle publish failed: $PUBLISH_ERR"
|
|
108
|
+
rm -rf "$TEMP_DIR"
|
|
109
|
+
trap - EXIT
|
|
110
|
+
if [[ "$ON_ERROR" == "exit" ]]; then
|
|
111
|
+
echo ""
|
|
112
|
+
echo "SETUP FAILED: A Pipeline Management BotDefinition is REQUIRED for user interaction."
|
|
113
|
+
exit 1
|
|
114
|
+
fi
|
|
115
|
+
return 1
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
msg_pass "Sales Agent published successfully"
|
|
119
|
+
|
|
120
|
+
# Query for the latest BotVersion with retry (async provisioning).
|
|
121
|
+
# Match the bot by AgentTemplate (the same field the scheduled flow uses),
|
|
122
|
+
# not by hardcoded DeveloperName — the bundle may publish under a different
|
|
123
|
+
# local name in some orgs.
|
|
124
|
+
msg_info "Querying for latest version..."
|
|
125
|
+
local LATEST_VERSION=""
|
|
126
|
+
for attempt in {1..3}; do
|
|
127
|
+
LATEST_VERSION=$(pm_bot_latest_version "$ORG_ALIAS")
|
|
128
|
+
|
|
129
|
+
if [[ -n "$LATEST_VERSION" ]]; then
|
|
130
|
+
break
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
if [[ $attempt -lt 3 ]]; then
|
|
134
|
+
echo " Version not yet available, retrying in 5s... (attempt $attempt/3)"
|
|
135
|
+
sleep 5
|
|
136
|
+
fi
|
|
137
|
+
done
|
|
138
|
+
|
|
139
|
+
if [[ -z "$LATEST_VERSION" ]]; then
|
|
140
|
+
msg_error "Could not query BotVersion after publish. BotDefinition may still be provisioning. Wait 30s and check Setup → Agents."
|
|
141
|
+
rm -rf "$TEMP_DIR"
|
|
142
|
+
trap - EXIT
|
|
143
|
+
if [[ "$ON_ERROR" == "exit" ]]; then
|
|
144
|
+
echo ""
|
|
145
|
+
echo "SETUP FAILED: A Pipeline Management BotDefinition is REQUIRED for user interaction."
|
|
146
|
+
exit 1
|
|
147
|
+
fi
|
|
148
|
+
return 1
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
# Resolve the actual published DeveloperName from AgentTemplate so we
|
|
152
|
+
# activate whatever name the bundle landed under (defaults to "SalesAgent"
|
|
153
|
+
# but the org may already have an upgraded NGA bot with a different name).
|
|
154
|
+
AGENT_API_NAME=$(pm_bot_developer_name "$ORG_ALIAS")
|
|
155
|
+
if [[ -z "$AGENT_API_NAME" ]]; then
|
|
156
|
+
AGENT_API_NAME="SalesAgent"
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
# Activate the agent with dynamic version
|
|
160
|
+
msg_info "Activating $AGENT_API_NAME version $LATEST_VERSION..."
|
|
161
|
+
local ACTIVATE_RESULT
|
|
162
|
+
ACTIVATE_RESULT=$(sf agent activate --api-name "$AGENT_API_NAME" --version "$LATEST_VERSION" --target-org "$ORG_ALIAS" --json 2>/dev/null || echo '{"status":1}')
|
|
163
|
+
|
|
164
|
+
if ! echo "$ACTIVATE_RESULT" | jq -e '.status == 0' >/dev/null 2>&1; then
|
|
165
|
+
local ACTIVATE_ERR
|
|
166
|
+
ACTIVATE_ERR=$(echo "$ACTIVATE_RESULT" | jq -r '.message // .result.message // .result.errors[0].message // "unknown"' 2>/dev/null || echo "unknown")
|
|
167
|
+
msg_error "Agent activation failed: $ACTIVATE_ERR. The agent was published but activation failed."
|
|
168
|
+
rm -rf "$TEMP_DIR"
|
|
169
|
+
trap - EXIT
|
|
170
|
+
if [[ "$ON_ERROR" == "exit" ]]; then
|
|
171
|
+
echo ""
|
|
172
|
+
echo "SETUP FAILED: A Pipeline Management BotDefinition is REQUIRED for user interaction."
|
|
173
|
+
exit 1
|
|
174
|
+
fi
|
|
175
|
+
return 1
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
msg_pass "$AGENT_API_NAME activated successfully (version $LATEST_VERSION)"
|
|
179
|
+
|
|
180
|
+
# Export version and resolved api-name for caller to use
|
|
181
|
+
export AGENT_VERSION="$LATEST_VERSION"
|
|
182
|
+
export AGENT_API_NAME
|
|
183
|
+
|
|
184
|
+
# Clean up temp directory before returning (EXIT trap only fires on exit, not return)
|
|
185
|
+
rm -rf "$TEMP_DIR"
|
|
186
|
+
trap - EXIT # Remove the trap since we've already cleaned up
|
|
187
|
+
|
|
188
|
+
return 0
|
|
189
|
+
}
|
package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/agent-detection.sh
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Shared library for Pipeline Management agent detection.
|
|
3
|
+
#
|
|
4
|
+
# The Pipeline Management scheduled flow (sales_pipe_mgmt__OppSuggGenSchFlow)
|
|
5
|
+
# finds the Sales Agent bot by querying BotDefinition.AgentTemplate, NOT by
|
|
6
|
+
# a hardcoded DeveloperName. Setup/verify scripts must match the same logic
|
|
7
|
+
# so they detect the agent regardless of what local name (SalesAgent,
|
|
8
|
+
# Sale_Agent, etc.) it was published with.
|
|
9
|
+
#
|
|
10
|
+
# The product currently supports two agent templates:
|
|
11
|
+
# - SalesMgmt__SalesAgent (legacy)
|
|
12
|
+
# - SalesMgmt__NGASalesAgent (upgraded NGA, used by our authoring bundle)
|
|
13
|
+
#
|
|
14
|
+
# Both flows (Test.flow, Process_Field_Update_Suggestions.flow) check
|
|
15
|
+
# AgentTemplate. We do the same here.
|
|
16
|
+
|
|
17
|
+
# Comma-separated AgentTemplate API names that identify a Pipeline Management
|
|
18
|
+
# Sales Agent. Quoted for SOQL IN(...) usage.
|
|
19
|
+
PM_AGENT_TEMPLATES_SOQL="'SalesMgmt__NGASalesAgent','SalesMgmt__SalesAgent'"
|
|
20
|
+
|
|
21
|
+
# Activation state lives on BotVersion.Status (Active|Inactive), not on
|
|
22
|
+
# BotDefinition (which only has IsDeleted).
|
|
23
|
+
#
|
|
24
|
+
# DEPRECATED: This correlated subquery hangs 60+ seconds on some orgs and must
|
|
25
|
+
# NOT be embedded in detection queries (removed in de77d520). Retained only so
|
|
26
|
+
# any lingering external caller still resolves the symbol; callers that need to
|
|
27
|
+
# know whether a bot is active should run a SEPARATE flat query on BotVersion
|
|
28
|
+
# (e.g. SELECT COUNT() FROM BotVersion WHERE BotDefinitionId='...' AND
|
|
29
|
+
# Status='Active') instead of splicing this into a BotDefinition WHERE clause.
|
|
30
|
+
PM_ACTIVE_BOT_FILTER="Id IN (SELECT BotDefinitionId FROM BotVersion WHERE Status = 'Active')"
|
|
31
|
+
|
|
32
|
+
# Print the DeveloperName of the Pipeline Management BotDefinition (if any),
|
|
33
|
+
# detected by AgentTemplate. Empty string when not found.
|
|
34
|
+
#
|
|
35
|
+
# NOTE: Does NOT filter by Active status to avoid slow/hanging subqueries.
|
|
36
|
+
# Callers should verify Active status separately if needed.
|
|
37
|
+
#
|
|
38
|
+
# Usage: name=$(pm_bot_developer_name "$ORG_ALIAS")
|
|
39
|
+
pm_bot_developer_name() {
|
|
40
|
+
local org_alias="$1"
|
|
41
|
+
sf data query \
|
|
42
|
+
--query "SELECT DeveloperName FROM BotDefinition WHERE AgentTemplate IN (${PM_AGENT_TEMPLATES_SOQL}) ORDER BY LastModifiedDate DESC LIMIT 1" \
|
|
43
|
+
--target-org "$org_alias" \
|
|
44
|
+
--json 2>/dev/null \
|
|
45
|
+
| jq -r '.result.records[0].DeveloperName // empty' 2>/dev/null \
|
|
46
|
+
|| echo ""
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Print the total number of Pipeline Management bots (BotDefinition rows
|
|
50
|
+
# matching PM AgentTemplate). Returns 0 if none, on error, or if the result
|
|
51
|
+
# can't be parsed.
|
|
52
|
+
#
|
|
53
|
+
# NOTE: Does NOT filter by Active status to avoid slow/hanging subqueries.
|
|
54
|
+
#
|
|
55
|
+
# Usage: count=$(pm_bot_count "$ORG_ALIAS")
|
|
56
|
+
pm_bot_count() {
|
|
57
|
+
local org_alias="$1"
|
|
58
|
+
local count
|
|
59
|
+
count=$(sf data query \
|
|
60
|
+
--query "SELECT Id FROM BotDefinition WHERE AgentTemplate IN (${PM_AGENT_TEMPLATES_SOQL})" \
|
|
61
|
+
--target-org "$org_alias" \
|
|
62
|
+
--json 2>/dev/null \
|
|
63
|
+
| jq -r '.result.totalSize // 0' 2>/dev/null \
|
|
64
|
+
| head -1 | tr -cd '0-9')
|
|
65
|
+
echo "${count:-0}"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Print the AgentTemplate API name of the active Pipeline Management bot.
|
|
69
|
+
# Used to keep the deployed flow's SalesManagementAgentTemplate constant
|
|
70
|
+
# in sync with the actual bot on the org (legacy SalesMgmt__SalesAgent vs
|
|
71
|
+
# upgraded SalesMgmt__NGASalesAgent), so the flow's BotDefinition lookup
|
|
72
|
+
# will match at runtime.
|
|
73
|
+
#
|
|
74
|
+
# Selection rule when both templates have a bot: prefer NGA (the forward
|
|
75
|
+
# direction; our authoring bundle publishes NGA). SOQL does not support CASE
|
|
76
|
+
# in ORDER BY, so we query NGA first and fall back to legacy.
|
|
77
|
+
#
|
|
78
|
+
# NOTE: Does NOT filter by Active status to avoid slow/hanging subqueries.
|
|
79
|
+
# Callers should verify Active status separately if needed.
|
|
80
|
+
#
|
|
81
|
+
# Exit codes distinguish failure modes for callers:
|
|
82
|
+
# 0 + stdout populated -> bot found, prints template name
|
|
83
|
+
# 1 + empty stdout -> no PM bot exists on the org
|
|
84
|
+
# 2 + empty stdout -> SOQL/auth/network failure (transient)
|
|
85
|
+
#
|
|
86
|
+
# Usage:
|
|
87
|
+
# if tpl=$(pm_bot_agent_template "$ORG_ALIAS"); then ... fi
|
|
88
|
+
# rc=$? # 0=ok, 1=no-bot, 2=query-failed
|
|
89
|
+
pm_bot_agent_template() {
|
|
90
|
+
local org_alias="$1"
|
|
91
|
+
local raw rc tpl
|
|
92
|
+
|
|
93
|
+
raw=$(sf data query \
|
|
94
|
+
--query "SELECT AgentTemplate FROM BotDefinition WHERE AgentTemplate = 'SalesMgmt__NGASalesAgent' ORDER BY LastModifiedDate DESC LIMIT 1" \
|
|
95
|
+
--target-org "$org_alias" \
|
|
96
|
+
--json 2>/dev/null)
|
|
97
|
+
rc=$?
|
|
98
|
+
if [[ $rc -ne 0 || -z "$raw" ]]; then
|
|
99
|
+
return 2
|
|
100
|
+
fi
|
|
101
|
+
tpl=$(echo "$raw" | jq -r '.result.records[0].AgentTemplate // empty' 2>/dev/null)
|
|
102
|
+
if [[ -n "$tpl" ]]; then
|
|
103
|
+
echo "$tpl"
|
|
104
|
+
return 0
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
raw=$(sf data query \
|
|
108
|
+
--query "SELECT AgentTemplate FROM BotDefinition WHERE AgentTemplate = 'SalesMgmt__SalesAgent' ORDER BY LastModifiedDate DESC LIMIT 1" \
|
|
109
|
+
--target-org "$org_alias" \
|
|
110
|
+
--json 2>/dev/null)
|
|
111
|
+
rc=$?
|
|
112
|
+
if [[ $rc -ne 0 || -z "$raw" ]]; then
|
|
113
|
+
return 2
|
|
114
|
+
fi
|
|
115
|
+
tpl=$(echo "$raw" | jq -r '.result.records[0].AgentTemplate // empty' 2>/dev/null)
|
|
116
|
+
if [[ -n "$tpl" ]]; then
|
|
117
|
+
echo "$tpl"
|
|
118
|
+
return 0
|
|
119
|
+
fi
|
|
120
|
+
return 1
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# Print the number of agent users that HOLD SalesManagementAgentUserPsg.
|
|
124
|
+
# This — not PSG existence — is a real "PM setup has run" signal, because the
|
|
125
|
+
# two PM PSGs (SalesManagementUserPsg / SalesManagementAgentUserPsg) ship with
|
|
126
|
+
# the Agentforce-for-Sales license and exist even in a never-configured org.
|
|
127
|
+
# Returns 0 if none / on error.
|
|
128
|
+
#
|
|
129
|
+
# Usage: n=$(pm_agent_user_holds_psg "$ORG_ALIAS")
|
|
130
|
+
pm_agent_user_holds_psg() {
|
|
131
|
+
local org_alias="$1" count
|
|
132
|
+
count=$(sf data query \
|
|
133
|
+
--query "SELECT Id FROM PermissionSetAssignment WHERE Assignee.Username LIKE '%salesmanagementagentuser%' AND PermissionSetGroup.DeveloperName='SalesManagementAgentUserPsg'" \
|
|
134
|
+
--target-org "$org_alias" --json 2>/dev/null \
|
|
135
|
+
| jq -r '.result.totalSize // 0' 2>/dev/null | head -1 | tr -cd '0-9')
|
|
136
|
+
echo "${count:-0}"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Print the number of scheduled flows cloned from the PM template
|
|
140
|
+
# (sales_pipe_mgmt__OppSuggGenSchFlow). A real "PM setup has run" signal.
|
|
141
|
+
# Returns 0 if none / on error.
|
|
142
|
+
#
|
|
143
|
+
# Usage: n=$(pm_flow_clone_exists "$ORG_ALIAS")
|
|
144
|
+
pm_flow_clone_exists() {
|
|
145
|
+
local org_alias="$1" count
|
|
146
|
+
count=$(sf data query \
|
|
147
|
+
--query "SELECT Id FROM FlowDefinitionView WHERE SourceTemplateId='sales_pipe_mgmt__OppSuggGenSchFlow' AND IsTemplate=false" \
|
|
148
|
+
--target-org "$org_alias" --json 2>/dev/null \
|
|
149
|
+
| jq -r '.result.totalSize // 0' 2>/dev/null | head -1 | tr -cd '0-9')
|
|
150
|
+
echo "${count:-0}"
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# Print the latest BotVersion VersionNumber for the PM bot (any status). Empty
|
|
154
|
+
# if no version exists. Note: This returns the most recent version regardless
|
|
155
|
+
# of activation status, which is necessary for the publish-then-activate flow
|
|
156
|
+
# where the freshly published version is still Inactive.
|
|
157
|
+
#
|
|
158
|
+
# Usage: ver=$(pm_bot_latest_version "$ORG_ALIAS")
|
|
159
|
+
pm_bot_latest_version() {
|
|
160
|
+
local org_alias="$1"
|
|
161
|
+
sf data query \
|
|
162
|
+
--query "SELECT VersionNumber FROM BotVersion WHERE BotDefinition.AgentTemplate IN (${PM_AGENT_TEMPLATES_SOQL}) ORDER BY VersionNumber DESC LIMIT 1" \
|
|
163
|
+
--target-org "$org_alias" \
|
|
164
|
+
--json 2>/dev/null \
|
|
165
|
+
| jq -r '.result.records[0].VersionNumber // empty' 2>/dev/null \
|
|
166
|
+
|| echo ""
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# Print the number of ACTIVE BotVersions for the PM bot. A bot can exist
|
|
170
|
+
# (BotDefinition present) yet have no active version — this is exactly the
|
|
171
|
+
# state auto-provisioning leaves the bot in: the BotVersion lands Inactive and
|
|
172
|
+
# must be activated before the scheduled flow will add the agent user to
|
|
173
|
+
# opportunity teams or Agent Access can be defined. Uses a SEPARATE flat query
|
|
174
|
+
# on BotVersion (never a correlated subquery — see PM_ACTIVE_BOT_FILTER note).
|
|
175
|
+
# Returns 0 if none / on error.
|
|
176
|
+
#
|
|
177
|
+
# Usage: n=$(pm_bot_active_version_count "$ORG_ALIAS")
|
|
178
|
+
pm_bot_active_version_count() {
|
|
179
|
+
local org_alias="$1" count
|
|
180
|
+
count=$(sf data query \
|
|
181
|
+
--query "SELECT Id FROM BotVersion WHERE BotDefinition.AgentTemplate IN (${PM_AGENT_TEMPLATES_SOQL}) AND Status = 'Active'" \
|
|
182
|
+
--target-org "$org_alias" \
|
|
183
|
+
--json 2>/dev/null \
|
|
184
|
+
| jq -r '.result.totalSize // 0' 2>/dev/null | head -1 | tr -cd '0-9')
|
|
185
|
+
echo "${count:-0}"
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# Ensure the PM bot has an ACTIVE BotVersion, activating the latest version if
|
|
189
|
+
# not. Idempotent: a no-op (returns 0) when an active version already exists.
|
|
190
|
+
# Uses `sf agent activate`, matching the activation path in
|
|
191
|
+
# publish_and_activate_agent. Prints progress via plain echo.
|
|
192
|
+
#
|
|
193
|
+
# Returns:
|
|
194
|
+
# 0 - an active version exists (already, or after successful activation)
|
|
195
|
+
# 1 - activation was attempted but failed, or no version exists to activate
|
|
196
|
+
#
|
|
197
|
+
# Usage: activate_pm_bot_if_inactive "$ORG_ALIAS"
|
|
198
|
+
activate_pm_bot_if_inactive() {
|
|
199
|
+
local org_alias="$1"
|
|
200
|
+
|
|
201
|
+
local active_count
|
|
202
|
+
active_count=$(pm_bot_active_version_count "$org_alias")
|
|
203
|
+
if [[ "$active_count" -ge 1 ]]; then
|
|
204
|
+
return 0
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
local latest_version api_name
|
|
208
|
+
latest_version=$(pm_bot_latest_version "$org_alias")
|
|
209
|
+
if [[ -z "$latest_version" ]]; then
|
|
210
|
+
echo " ERROR: BotDefinition exists but has no BotVersion to activate."
|
|
211
|
+
return 1
|
|
212
|
+
fi
|
|
213
|
+
|
|
214
|
+
api_name=$(pm_bot_developer_name "$org_alias")
|
|
215
|
+
[[ -z "$api_name" ]] && api_name="SalesAgent"
|
|
216
|
+
|
|
217
|
+
echo " Activating $api_name version $latest_version (auto-provisioned version was Inactive)..."
|
|
218
|
+
local activate_result
|
|
219
|
+
activate_result=$(sf agent activate --api-name "$api_name" --version "$latest_version" --target-org "$org_alias" --json 2>/dev/null || echo '{"status":1}')
|
|
220
|
+
|
|
221
|
+
if ! echo "$activate_result" | jq -e '.status == 0' >/dev/null 2>&1; then
|
|
222
|
+
local activate_err
|
|
223
|
+
activate_err=$(echo "$activate_result" | jq -r '.message // .result.message // .result.errors[0].message // "unknown"' 2>/dev/null || echo "unknown")
|
|
224
|
+
echo " ERROR: Agent activation failed: $activate_err"
|
|
225
|
+
return 1
|
|
226
|
+
fi
|
|
227
|
+
|
|
228
|
+
# Confirm the activation took effect (async on some orgs).
|
|
229
|
+
local attempt
|
|
230
|
+
for attempt in {1..3}; do
|
|
231
|
+
active_count=$(pm_bot_active_version_count "$org_alias")
|
|
232
|
+
[[ "$active_count" -ge 1 ]] && return 0
|
|
233
|
+
[[ $attempt -lt 3 ]] && sleep 5
|
|
234
|
+
done
|
|
235
|
+
|
|
236
|
+
# sf agent activate returned success; treat as activated even if the
|
|
237
|
+
# confirmation query lagged.
|
|
238
|
+
return 0
|
|
239
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# shared/flow-builder.sh — build the suggestion flow's field collection from a
|
|
3
|
+
# selected set of Opportunity fields.
|
|
4
|
+
#
|
|
5
|
+
# The base flow template (assets/pipeline_management_flow.flow-meta.xml) ships
|
|
6
|
+
# with a hardcoded AddOppFieldsToCollection assignment wiring BOTH NextStep and
|
|
7
|
+
# StageName. Historically setup deployed both and then STRIPPED the unwanted one
|
|
8
|
+
# (see strip_flow_field in setup-all.sh, still used for the repair path). For a
|
|
9
|
+
# greenfield setup we instead build the flow with ONLY the selected fields from
|
|
10
|
+
# the start — no deploy-then-strip round trip.
|
|
11
|
+
#
|
|
12
|
+
# This file only DEFINES a function; it makes no org calls and mutates no files
|
|
13
|
+
# when sourced. build_field_collection reads the base template and writes a NEW
|
|
14
|
+
# output file; it never edits the template in place.
|
|
15
|
+
|
|
16
|
+
# build_field_collection <comma-separated-fields> <base-template> <output-file>
|
|
17
|
+
#
|
|
18
|
+
# Rewrites the AddOppFieldsToCollection assignment so its <assignmentItems>
|
|
19
|
+
# contain exactly the given fields (in order, de-duplicated by the caller).
|
|
20
|
+
# Uses python3 for a namespace-safe edit — the same requirement as
|
|
21
|
+
# strip_flow_field. Returns non-zero (without writing) if python3 is missing,
|
|
22
|
+
# the template is unreadable, the assignment/fields are absent, or the write
|
|
23
|
+
# fails, so callers can fall back rather than deploy a corrupt/empty flow.
|
|
24
|
+
#
|
|
25
|
+
# A zero-item AddOppFieldsToCollection is INVALID Flow metadata and fails to
|
|
26
|
+
# deploy, so this function refuses to write when the field list is empty.
|
|
27
|
+
build_field_collection() {
|
|
28
|
+
local fields="$1" base_template="$2" output_file="$3"
|
|
29
|
+
|
|
30
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
31
|
+
echo " [FAIL] python3 not available — cannot build flow field collection." >&2
|
|
32
|
+
return 1
|
|
33
|
+
fi
|
|
34
|
+
if [[ ! -f "$base_template" ]]; then
|
|
35
|
+
echo " [FAIL] Base flow template not found: $base_template" >&2
|
|
36
|
+
return 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
python3 - "$base_template" "$output_file" "$fields" <<'PYEOF'
|
|
40
|
+
import sys, xml.etree.ElementTree as ET
|
|
41
|
+
|
|
42
|
+
base_template, output_file, raw_fields = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
43
|
+
|
|
44
|
+
# Order-preserving de-dup of trimmed, non-empty field names.
|
|
45
|
+
fields = []
|
|
46
|
+
for f in raw_fields.split(","):
|
|
47
|
+
f = f.strip()
|
|
48
|
+
if f and f not in fields:
|
|
49
|
+
fields.append(f)
|
|
50
|
+
|
|
51
|
+
if not fields:
|
|
52
|
+
sys.stderr.write(" [FAIL] No fields provided — refusing to write an empty AddOppFieldsToCollection.\n")
|
|
53
|
+
sys.exit(2)
|
|
54
|
+
|
|
55
|
+
ns = "http://soap.sforce.com/2006/04/metadata"
|
|
56
|
+
ET.register_namespace("", ns)
|
|
57
|
+
try:
|
|
58
|
+
tree = ET.parse(base_template)
|
|
59
|
+
except Exception as e: # noqa: BLE001
|
|
60
|
+
sys.stderr.write(" [FAIL] Could not parse base flow template: %s\n" % e)
|
|
61
|
+
sys.exit(3)
|
|
62
|
+
|
|
63
|
+
root = tree.getroot()
|
|
64
|
+
q = lambda t: "{%s}%s" % (ns, t)
|
|
65
|
+
|
|
66
|
+
target = None
|
|
67
|
+
for assign in root.findall(q("assignments")):
|
|
68
|
+
name_elem = assign.find(q("name"))
|
|
69
|
+
if name_elem is not None and name_elem.text == "AddOppFieldsToCollection":
|
|
70
|
+
target = assign
|
|
71
|
+
break
|
|
72
|
+
|
|
73
|
+
if target is None:
|
|
74
|
+
sys.stderr.write(" [FAIL] AddOppFieldsToCollection assignment not found in template.\n")
|
|
75
|
+
sys.exit(4)
|
|
76
|
+
|
|
77
|
+
# Drop existing assignmentItems, then rebuild from the selected fields.
|
|
78
|
+
for item in list(target.findall(q("assignmentItems"))):
|
|
79
|
+
target.remove(item)
|
|
80
|
+
|
|
81
|
+
# The FlowAssignment schema requires every <assignmentItems> to be contiguous
|
|
82
|
+
# and positioned BEFORE <connector>. A plain ET.SubElement(target, ...) appends
|
|
83
|
+
# to the end of the element — AFTER <connector> when the template has one — which
|
|
84
|
+
# the Metadata API rejects with "Element assignmentItems is duplicated at this
|
|
85
|
+
# location". Find the connector's index and insert each new block just before it
|
|
86
|
+
# (falling back to append when there is no connector).
|
|
87
|
+
conn_idx = next((i for i, c in enumerate(list(target)) if c.tag == q("connector")), None)
|
|
88
|
+
insert_at = conn_idx
|
|
89
|
+
for field in fields:
|
|
90
|
+
item = ET.Element(q("assignmentItems"))
|
|
91
|
+
ref = ET.SubElement(item, q("assignToReference"))
|
|
92
|
+
ref.text = "OpportunityFields"
|
|
93
|
+
op = ET.SubElement(item, q("operator"))
|
|
94
|
+
op.text = "Add"
|
|
95
|
+
val = ET.SubElement(item, q("value"))
|
|
96
|
+
sv = ET.SubElement(val, q("stringValue"))
|
|
97
|
+
sv.text = field
|
|
98
|
+
if insert_at is None:
|
|
99
|
+
target.append(item)
|
|
100
|
+
else:
|
|
101
|
+
target.insert(insert_at, item)
|
|
102
|
+
insert_at += 1
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
tree.write(output_file, xml_declaration=True, encoding="UTF-8")
|
|
106
|
+
except Exception as e: # noqa: BLE001
|
|
107
|
+
sys.stderr.write(" [FAIL] Could not write flow output file: %s\n" % e)
|
|
108
|
+
sys.exit(5)
|
|
109
|
+
|
|
110
|
+
sys.exit(0)
|
|
111
|
+
PYEOF
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# strip_flow_field <flow-file> <field> — remove the matching <assignmentItems>
|
|
115
|
+
# from the AddOppFieldsToCollection assignment of an EXISTING flow file, in place.
|
|
116
|
+
# This is the "fix method" used by the REPAIR path (a flow already deployed with
|
|
117
|
+
# fields the admin no longer wants). Greenfield never strips — it builds only the
|
|
118
|
+
# selected fields via build_field_collection. Requires python3 for a
|
|
119
|
+
# namespace-safe edit; if it is unavailable the field is left wired rather than
|
|
120
|
+
# risk corrupting the XML with a regex. Returns 0 if a field was removed, 2 if
|
|
121
|
+
# the field was not present (nothing to strip), non-zero on error.
|
|
122
|
+
strip_flow_field() {
|
|
123
|
+
local flow_file="$1" field="$2"
|
|
124
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
125
|
+
echo " [WARN] python3 not available — cannot strip '$field' from the flow; leaving it wired." >&2
|
|
126
|
+
return 1
|
|
127
|
+
fi
|
|
128
|
+
python3 - "$flow_file" "$field" <<'PYEOF'
|
|
129
|
+
import sys, xml.etree.ElementTree as ET
|
|
130
|
+
flow_file, field = sys.argv[1], sys.argv[2]
|
|
131
|
+
ns = "http://soap.sforce.com/2006/04/metadata"
|
|
132
|
+
ET.register_namespace("", ns)
|
|
133
|
+
tree = ET.parse(flow_file)
|
|
134
|
+
root = tree.getroot()
|
|
135
|
+
q = lambda t: "{%s}%s" % (ns, t)
|
|
136
|
+
removed = 0
|
|
137
|
+
for assign in root.findall(q("assignments")):
|
|
138
|
+
name = assign.find(q("name"))
|
|
139
|
+
if name is None or name.text != "AddOppFieldsToCollection":
|
|
140
|
+
continue
|
|
141
|
+
for item in list(assign.findall(q("assignmentItems"))):
|
|
142
|
+
sv = item.find("%s/%s" % (q("value"), q("stringValue")))
|
|
143
|
+
if sv is not None and sv.text == field:
|
|
144
|
+
assign.remove(item)
|
|
145
|
+
removed += 1
|
|
146
|
+
if removed:
|
|
147
|
+
tree.write(flow_file, xml_declaration=True, encoding="UTF-8")
|
|
148
|
+
sys.exit(0 if removed else 2)
|
|
149
|
+
PYEOF
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# add_flow_field <flow-file> <field> — idempotently ADD a field's <assignmentItems>
|
|
153
|
+
# to the AddOppFieldsToCollection assignment of an EXISTING flow file, in place.
|
|
154
|
+
# The symmetric counterpart to strip_flow_field, used by the REPAIR path to wire a
|
|
155
|
+
# newly-selected field into an already-deployed flow without a full rebuild (a full
|
|
156
|
+
# rebuild would discard the org-specific SalesManagementAgentTemplate constant the
|
|
157
|
+
# retrieved flow carries). No-op if the field is already wired. Returns 0 if added,
|
|
158
|
+
# 2 if it was already present, non-zero on error.
|
|
159
|
+
add_flow_field() {
|
|
160
|
+
local flow_file="$1" field="$2"
|
|
161
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
162
|
+
echo " [WARN] python3 not available — cannot add '$field' to the flow." >&2
|
|
163
|
+
return 1
|
|
164
|
+
fi
|
|
165
|
+
python3 - "$flow_file" "$field" <<'PYEOF'
|
|
166
|
+
import sys, xml.etree.ElementTree as ET
|
|
167
|
+
flow_file, field = sys.argv[1], sys.argv[2]
|
|
168
|
+
ns = "http://soap.sforce.com/2006/04/metadata"
|
|
169
|
+
ET.register_namespace("", ns)
|
|
170
|
+
tree = ET.parse(flow_file)
|
|
171
|
+
root = tree.getroot()
|
|
172
|
+
q = lambda t: "{%s}%s" % (ns, t)
|
|
173
|
+
target = None
|
|
174
|
+
for assign in root.findall(q("assignments")):
|
|
175
|
+
name = assign.find(q("name"))
|
|
176
|
+
if name is not None and name.text == "AddOppFieldsToCollection":
|
|
177
|
+
target = assign
|
|
178
|
+
break
|
|
179
|
+
if target is None:
|
|
180
|
+
sys.stderr.write(" [FAIL] AddOppFieldsToCollection assignment not found.\n")
|
|
181
|
+
sys.exit(4)
|
|
182
|
+
children = list(target)
|
|
183
|
+
last_ai_idx = -1
|
|
184
|
+
for idx, item in enumerate(children):
|
|
185
|
+
if item.tag != q("assignmentItems"):
|
|
186
|
+
continue
|
|
187
|
+
last_ai_idx = idx
|
|
188
|
+
sv = item.find("%s/%s" % (q("value"), q("stringValue")))
|
|
189
|
+
if sv is not None and sv.text == field:
|
|
190
|
+
sys.exit(2) # already wired
|
|
191
|
+
|
|
192
|
+
# Build the new assignmentItems block.
|
|
193
|
+
item = ET.Element(q("assignmentItems"))
|
|
194
|
+
ref = ET.SubElement(item, q("assignToReference")); ref.text = "OpportunityFields"
|
|
195
|
+
op = ET.SubElement(item, q("operator")); op.text = "Add"
|
|
196
|
+
val = ET.SubElement(item, q("value"))
|
|
197
|
+
sv = ET.SubElement(val, q("stringValue")); sv.text = field
|
|
198
|
+
|
|
199
|
+
# The FlowAssignment schema requires every <assignmentItems> to be contiguous
|
|
200
|
+
# and positioned before <connector>. A plain append() lands the new block at
|
|
201
|
+
# the end of the element — AFTER <connector> when one exists — which the
|
|
202
|
+
# Metadata API rejects with "Element assignmentItems is duplicated at this
|
|
203
|
+
# location". Insert immediately after the last existing assignmentItems; if
|
|
204
|
+
# there are none, insert just before <connector> (else fall back to append).
|
|
205
|
+
if last_ai_idx >= 0:
|
|
206
|
+
target.insert(last_ai_idx + 1, item)
|
|
207
|
+
else:
|
|
208
|
+
conn_idx = next((i for i, c in enumerate(children) if c.tag == q("connector")), None)
|
|
209
|
+
if conn_idx is not None:
|
|
210
|
+
target.insert(conn_idx, item)
|
|
211
|
+
else:
|
|
212
|
+
target.append(item)
|
|
213
|
+
tree.write(flow_file, xml_declaration=True, encoding="UTF-8")
|
|
214
|
+
sys.exit(0)
|
|
215
|
+
PYEOF
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# flow_wired_fields <flow-file> — print the field API names currently wired into
|
|
219
|
+
# AddOppFieldsToCollection, one per line (in document order). Used by the repair
|
|
220
|
+
# path to diff the deployed flow against the admin's selected set.
|
|
221
|
+
flow_wired_fields() {
|
|
222
|
+
local flow_file="$1"
|
|
223
|
+
command -v python3 >/dev/null 2>&1 || return 1
|
|
224
|
+
python3 - "$flow_file" <<'PYEOF'
|
|
225
|
+
import sys, xml.etree.ElementTree as ET
|
|
226
|
+
ns = "http://soap.sforce.com/2006/04/metadata"
|
|
227
|
+
tree = ET.parse(sys.argv[1])
|
|
228
|
+
q = lambda t: "{%s}%s" % (ns, t)
|
|
229
|
+
for assign in tree.getroot().findall(q("assignments")):
|
|
230
|
+
name = assign.find(q("name"))
|
|
231
|
+
if name is None or name.text != "AddOppFieldsToCollection":
|
|
232
|
+
continue
|
|
233
|
+
for item in assign.findall(q("assignmentItems")):
|
|
234
|
+
sv = item.find("%s/%s" % (q("value"), q("stringValue")))
|
|
235
|
+
if sv is not None and sv.text:
|
|
236
|
+
print(sv.text)
|
|
237
|
+
PYEOF
|
|
238
|
+
}
|