@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.
Files changed (98) hide show
  1. package/package.json +1 -1
  2. package/skills/agentforce-bot-upgrade/SKILL.md +217 -0
  3. package/skills/agentforce-bot-upgrade/references/extraction-blueprint.md +242 -0
  4. package/skills/agentforce-bot-upgrade/references/generate-agent-spec.md +128 -0
  5. package/skills/agentforce-bot-upgrade/references/handoff-output-format.md +208 -0
  6. package/skills/agentforce-bot-upgrade/references/input-contract.md +101 -0
  7. package/skills/agentforce-bot-upgrade/references/mapping-rules.md +113 -0
  8. package/skills/agentforce-bot-upgrade/references/planner-workflow-reference.md +78 -0
  9. package/skills/agentforce-bot-upgrade/references/post-conversion-enhancements-reference.md +67 -0
  10. package/skills/agentforce-bot-upgrade/references/quality-checklist.md +33 -0
  11. package/skills/agentforce-bot-upgrade/references/sf-cli-bot-reference.md +116 -0
  12. package/skills/dx-devops-work-item-manage/SKILL.md +270 -0
  13. package/skills/dx-devops-work-item-manage/examples/common-workflows.md +479 -0
  14. package/skills/dx-devops-work-item-manage/references/cli-commands.md +476 -0
  15. package/skills/dx-org-manage/SKILL.md +8 -5
  16. package/skills/dx-org-trial-expiration-check/SKILL.md +239 -0
  17. package/skills/dx-org-trial-expiration-check/scripts/check_expiration.sh +588 -0
  18. package/skills/experience-ui-bundle-app-coordinate/SKILL.md +12 -9
  19. package/skills/experience-ui-bundle-custom-app-generate/SKILL.md +29 -5
  20. package/skills/experience-ui-bundle-custom-app-generate/references/configure-metadata-custom-application.md +1 -1
  21. package/skills/experience-ui-bundle-custom-app-generate/scripts/resolve-uibundle-path.sh +30 -0
  22. package/skills/experience-ui-bundle-features-generate/SKILL.md +86 -11
  23. package/skills/experience-ui-bundle-features-generate/references/conflict-resolution-schema.json +20 -0
  24. package/skills/experience-ui-bundle-features-generate/scripts/verify-react-bundle.sh +14 -0
  25. package/skills/experience-ui-bundle-frontend-generate/SKILL.md +6 -1
  26. package/skills/experience-ui-bundle-frontend-generate/references/component.md +34 -1
  27. package/skills/experience-ui-bundle-frontend-generate/references/page.md +1 -1
  28. package/skills/experience-ui-bundle-project-generate/SKILL.md +111 -0
  29. package/skills/experience-ui-bundle-project-generate/scripts/flatten-project.mjs +33 -0
  30. package/skills/platform-custom-lightning-type-generate/SKILL.md +9 -14
  31. package/skills/platform-custom-lightning-type-generate/references/widget-rendition.md +27 -111
  32. package/skills/platform-lightning-type-widget-coordinate/SKILL.md +214 -0
  33. package/skills/platform-lightning-type-widget-coordinate/examples/existing-lightning-type-with-widget-prompt.md +80 -0
  34. package/skills/platform-lightning-type-widget-coordinate/examples/new-lightning-type-with-widget-prompt.md +75 -0
  35. package/skills/platform-lightning-type-widget-coordinate/references/build-plan-format.md +67 -0
  36. package/skills/platform-lightning-type-widget-coordinate/references/lightning-type-discovery.md +124 -0
  37. package/skills/platform-lightning-type-widget-coordinate/references/validation-gates.md +130 -0
  38. package/skills/platform-sharing-owd-configure/SKILL.md +142 -0
  39. package/skills/platform-sharing-owd-configure/examples/get_owd_output.md +38 -0
  40. package/skills/platform-sharing-owd-configure/examples/update_owd_output.md +44 -0
  41. package/skills/platform-sharing-owd-configure/references/access_levels.md +49 -0
  42. package/skills/platform-sharing-owd-configure/references/metadata_api_approach.md +69 -0
  43. package/skills/platform-sharing-rules-generate/SKILL.md +89 -19
  44. package/skills/platform-sharing-rules-generate/examples/test-cases.md +381 -0
  45. package/skills/platform-widget-generate/SKILL.md +241 -0
  46. package/skills/platform-widget-generate/examples/conditional.json +73 -0
  47. package/skills/platform-widget-generate/examples/list-with-foreach.json +66 -0
  48. package/skills/platform-widget-generate/examples/single-object.json +64 -0
  49. package/skills/platform-widget-generate/references/schema-from-lightning-type.md +76 -0
  50. package/skills/platform-widget-generate/references/widget-bundle-layout.md +116 -0
  51. package/skills/platform-widget-generate/references/widget-meta-directives.md +130 -0
  52. package/skills/sales-agentforce-pipeline-management-configure/SKILL.md +268 -0
  53. package/skills/sales-agentforce-pipeline-management-configure/assets/field-completion-template.genAiPromptTemplate-meta.xml +141 -0
  54. package/skills/sales-agentforce-pipeline-management-configure/assets/pipeline_management_flow.flow-meta.xml +548 -0
  55. package/skills/sales-agentforce-pipeline-management-configure/assets/sales_management_agent.agent +2938 -0
  56. package/skills/sales-agentforce-pipeline-management-configure/assets/sales_management_agent.bundle-meta.xml +4 -0
  57. package/skills/sales-agentforce-pipeline-management-configure/assets/sfdx-project.json +12 -0
  58. package/skills/sales-agentforce-pipeline-management-configure/assets/stage-descriptions/bant.txt +11 -0
  59. package/skills/sales-agentforce-pipeline-management-configure/assets/stage-descriptions/meddic.txt +11 -0
  60. package/skills/sales-agentforce-pipeline-management-configure/assets/stage-descriptions/spiced.txt +11 -0
  61. package/skills/sales-agentforce-pipeline-management-configure/examples/custom-prompt-instructions.md +317 -0
  62. package/skills/sales-agentforce-pipeline-management-configure/references/admin-communication.md +355 -0
  63. package/skills/sales-agentforce-pipeline-management-configure/references/agent-creation.md +383 -0
  64. package/skills/sales-agentforce-pipeline-management-configure/references/auth-and-cli.md +146 -0
  65. package/skills/sales-agentforce-pipeline-management-configure/references/automation-matrix.md +23 -0
  66. package/skills/sales-agentforce-pipeline-management-configure/references/autonomous-updates.md +215 -0
  67. package/skills/sales-agentforce-pipeline-management-configure/references/canonical-agent-path.md +75 -0
  68. package/skills/sales-agentforce-pipeline-management-configure/references/data-sources.md +155 -0
  69. package/skills/sales-agentforce-pipeline-management-configure/references/field-completion-prompt-template.md +120 -0
  70. package/skills/sales-agentforce-pipeline-management-configure/references/field-completion-prompts.md +149 -0
  71. package/skills/sales-agentforce-pipeline-management-configure/references/flags.md +13 -0
  72. package/skills/sales-agentforce-pipeline-management-configure/references/flow-clone-from-template.md +447 -0
  73. package/skills/sales-agentforce-pipeline-management-configure/references/metadata-inventory.md +330 -0
  74. package/skills/sales-agentforce-pipeline-management-configure/references/opportunity-stages.md +246 -0
  75. package/skills/sales-agentforce-pipeline-management-configure/references/repair-diagnostics.md +265 -0
  76. package/skills/sales-agentforce-pipeline-management-configure/references/setup-order.md +263 -0
  77. package/skills/sales-agentforce-pipeline-management-configure/references/soap-api-enablement.md +528 -0
  78. package/skills/sales-agentforce-pipeline-management-configure/scripts/CHANGES.md +180 -0
  79. package/skills/sales-agentforce-pipeline-management-configure/scripts/README.md +56 -0
  80. package/skills/sales-agentforce-pipeline-management-configure/scripts/add-field-suggestion.sh +679 -0
  81. package/skills/sales-agentforce-pipeline-management-configure/scripts/create-agent.sh +302 -0
  82. package/skills/sales-agentforce-pipeline-management-configure/scripts/create-flow.sh +250 -0
  83. package/skills/sales-agentforce-pipeline-management-configure/scripts/define-agent-access.sh +430 -0
  84. package/skills/sales-agentforce-pipeline-management-configure/scripts/deploy-settings.sh +77 -0
  85. package/skills/sales-agentforce-pipeline-management-configure/scripts/enable-deal-agent.sh +241 -0
  86. package/skills/sales-agentforce-pipeline-management-configure/scripts/enable-prerequisites.sh +538 -0
  87. package/skills/sales-agentforce-pipeline-management-configure/scripts/flow-debug-and-verify.sh +385 -0
  88. package/skills/sales-agentforce-pipeline-management-configure/scripts/retrieve-settings.sh +131 -0
  89. package/skills/sales-agentforce-pipeline-management-configure/scripts/setup-all.sh +2791 -0
  90. package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/agent-bundle-publish.sh +189 -0
  91. package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/agent-detection.sh +239 -0
  92. package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/flow-builder.sh +238 -0
  93. package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/psg.sh +48 -0
  94. package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/soap.sh +140 -0
  95. package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/stage-descriptions.sh +448 -0
  96. package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/test-opp.sh +166 -0
  97. package/skills/sales-agentforce-pipeline-management-configure/scripts/verify-all.sh +636 -0
  98. package/skills/sales-agentforce-pipeline-management-configure/scripts/verify-prompt-generation.sh +346 -0
@@ -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
+ }
@@ -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
+ }