@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
|
@@ -0,0 +1,2791 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Master orchestration script for Pipeline Management end-to-end setup
|
|
3
|
+
# Handles both greenfield and partially-configured orgs.
|
|
4
|
+
#
|
|
5
|
+
# Architecture: Each phase uses a "harness loop" pattern:
|
|
6
|
+
# 1. Check if already done (skip if yes)
|
|
7
|
+
# 2. Try primary approach (SOAP Metadata API)
|
|
8
|
+
# 3. If primary fails, try fallback (CLI retrieve/modify/deploy)
|
|
9
|
+
# 4. If fallback fails, try tertiary (Tooling API / REST)
|
|
10
|
+
# 5. VERIFY the result via readMetadata before proceeding
|
|
11
|
+
# 6. Only report BLOCKER after ALL approaches exhausted
|
|
12
|
+
#
|
|
13
|
+
# Working Directory Note:
|
|
14
|
+
# Agent bundle publishing uses isolated temp directories with automatic cleanup.
|
|
15
|
+
# Settings/flow deployment operations may create force-app/ in the current directory
|
|
16
|
+
# when SOAP/Tooling API approaches fail and CLI deploy is attempted as fallback.
|
|
17
|
+
#
|
|
18
|
+
# Usage: ./setup-all.sh <org-alias> [flags]
|
|
19
|
+
#
|
|
20
|
+
# Flags (all optional; SAFE by default):
|
|
21
|
+
# --autonomous Enable autonomous field updates (agent auto-applies
|
|
22
|
+
# without human review). OFF by default — suggestions only.
|
|
23
|
+
# --create-stage-descriptions Create sales-methodology stage descriptions (needed only
|
|
24
|
+
# for StageName suggestions). OFF by default.
|
|
25
|
+
# --non-interactive | --yes Run without prompts. Combined with the flags above this
|
|
26
|
+
# gives fully unattended setup; without them the run stays
|
|
27
|
+
# on the safe (suggestion-only, no stage-desc) defaults.
|
|
28
|
+
# --users "a@x.com,b@x.com" Explicit comma-separated usernames to also grant the
|
|
29
|
+
# SalesManagementUserPsg (in addition to the running user).
|
|
30
|
+
# This is the ONLY non-interactive way to add users — the
|
|
31
|
+
# script NEVER queries all/standard users and bulk-assigns.
|
|
32
|
+
|
|
33
|
+
set -euo pipefail
|
|
34
|
+
|
|
35
|
+
# Require bash 4.0+ for associative arrays (macOS ships bash 3.2 by default)
|
|
36
|
+
if [[ ${BASH_VERSINFO[0]} -lt 4 ]]; then
|
|
37
|
+
echo "ERROR: bash 4.0+ required. Your version: $BASH_VERSION"
|
|
38
|
+
echo "On macOS, install via: brew install bash"
|
|
39
|
+
echo "Then run: /opt/homebrew/bin/bash $0 $*"
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# Set consistent locale to prevent jq parsing issues with special characters
|
|
44
|
+
export LC_ALL=en_US.UTF-8
|
|
45
|
+
export LANG=en_US.UTF-8
|
|
46
|
+
|
|
47
|
+
# sf CLI can emit ANSI color codes inside --json stdout, breaking jq parsing.
|
|
48
|
+
# 2>/dev/null only strips stderr; these env vars are the reliable fix (see PM notes).
|
|
49
|
+
# Exported here so they propagate to sourced libs and child bash subprocesses.
|
|
50
|
+
export NO_COLOR=1
|
|
51
|
+
export FORCE_COLOR=0
|
|
52
|
+
|
|
53
|
+
ORG_ALIAS="${1:-}"
|
|
54
|
+
# Validate org alias (prevent shell injection via metacharacters)
|
|
55
|
+
if [[ -n "$ORG_ALIAS" && ! "$ORG_ALIAS" =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]]; then
|
|
56
|
+
echo "ERROR: Invalid org alias. Only alphanumeric, dots, hyphens, and underscores allowed (must start with alphanumeric)."
|
|
57
|
+
exit 1
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# --- Consent flags (SAFE by default) ---
|
|
61
|
+
# Autonomous updates and stage-description creation are BOTH opt-in. In non-interactive
|
|
62
|
+
# mode we never enable them implicitly; the operator must pass the explicit flag.
|
|
63
|
+
ENABLE_AUTONOMOUS=false # --autonomous
|
|
64
|
+
CREATE_STAGE_DESCRIPTIONS=false # --create-stage-descriptions
|
|
65
|
+
NON_INTERACTIVE=false # --non-interactive | --yes (also implied when stdin is not a TTY)
|
|
66
|
+
USERS_LIST="" # --users "a@x.com,b@x.com" — explicit PSG grantees (never bulk)
|
|
67
|
+
|
|
68
|
+
# --- Field selection (3-phase setup) ---
|
|
69
|
+
# Which Opportunity fields the suggestion flow should manage. Chosen UP FRONT
|
|
70
|
+
# (before the flow is built) so the flow is built with ONLY these fields — no
|
|
71
|
+
# deploy-then-strip. At least one field is REQUIRED; setup ABORTS if none are
|
|
72
|
+
# selected (there is no silent default). OOTB fields (NextStep, StageName) count
|
|
73
|
+
# toward the 5-field cap. Per-field goal/instruction customization is collected
|
|
74
|
+
# into FIELD_PROMPTS keyed "<Field>_goal" / "<Field>_instruction".
|
|
75
|
+
SELECTED_FIELDS="" # --fields "NextStep,StageName,Risk__c"
|
|
76
|
+
SKIP_PROMPT_VERIFICATION=false # --skip-prompt-verification (bypass the Phase 2e gate)
|
|
77
|
+
declare -A FIELD_PROMPTS # per-field goal/instruction, from --field-goal/--field-instruction
|
|
78
|
+
readonly MAX_SUGGESTION_FIELDS=5
|
|
79
|
+
|
|
80
|
+
# --- Phase split (agent-driven interactive tune-loop) ---
|
|
81
|
+
# The full run is one long non-interactive pass, which (a) can't pause for the
|
|
82
|
+
# agent to tune each field's prompt with the user and (b) can exceed the harness
|
|
83
|
+
# tool timeout. --through-phase / --from-phase cut the run at the prompt boundary
|
|
84
|
+
# so the AGENT owns the interactive tune-loop BETWEEN two calls:
|
|
85
|
+
# Call 1: --through-phase prompts → enable, provision, deploy+activate templates,
|
|
86
|
+
# then STOP before the flow is built (context phases 0/1/1.7 always run).
|
|
87
|
+
# (agent tunes each field via add-field-suggestion.sh --verify-with-note --note …)
|
|
88
|
+
# Call 2: --from-phase flow → build+activate flow, agent, PSG, final verify.
|
|
89
|
+
# Both calls are idempotent, so this is a resumable checkpoint, not new state.
|
|
90
|
+
# Recognized cut points: "prompts" (end of Phase 4d) and "flow" (start of Phase 5).
|
|
91
|
+
RUN_FROM_PHASE="" # --from-phase flow (skip everything before Phase 5)
|
|
92
|
+
RUN_THROUGH_PHASE="" # --through-phase prompts (stop after templates, before flow)
|
|
93
|
+
|
|
94
|
+
# --- License preflight (agent's first action, before any clarifying question) ---
|
|
95
|
+
# --check-license runs ONLY auth + the Phase 0.5 capability gate, then exits. It
|
|
96
|
+
# takes no --fields (so it can run before the field questions the gate must
|
|
97
|
+
# precede), asks nothing, and mutates nothing. The SKILL drives this FIRST so an
|
|
98
|
+
# unlicensed org is blocked before the user is asked anything.
|
|
99
|
+
CHECK_LICENSE_ONLY=false # --check-license
|
|
100
|
+
|
|
101
|
+
# Parse flags after the positional org alias. `shift` past $1 first if present.
|
|
102
|
+
if [[ $# -ge 1 ]]; then shift || true; fi
|
|
103
|
+
while [[ $# -gt 0 ]]; do
|
|
104
|
+
case "$1" in
|
|
105
|
+
--autonomous) ENABLE_AUTONOMOUS=true; shift ;;
|
|
106
|
+
--create-stage-descriptions) CREATE_STAGE_DESCRIPTIONS=true; shift ;;
|
|
107
|
+
--non-interactive|--yes) NON_INTERACTIVE=true; shift ;;
|
|
108
|
+
--check-license) CHECK_LICENSE_ONLY=true; shift ;;
|
|
109
|
+
--skip-prompt-verification) SKIP_PROMPT_VERIFICATION=true; shift ;;
|
|
110
|
+
--users) { [[ $# -ge 2 ]] && [[ "$2" != --* ]]; } || { echo "ERROR: --users requires a value (comma-separated usernames), e.g. --users \"a@x.com,b@x.com\""; exit 1; }
|
|
111
|
+
IFS=',' read -ra _TMP_USERS <<< "$2"
|
|
112
|
+
for _u in "${_TMP_USERS[@]}"; do
|
|
113
|
+
_u=$(echo "$_u" | xargs)
|
|
114
|
+
[[ "$_u" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]] || { echo "ERROR: Invalid email format in --users: '$_u'. Each value must be a valid email address."; exit 1; }
|
|
115
|
+
done
|
|
116
|
+
USERS_LIST="$2"; shift 2 ;;
|
|
117
|
+
--fields) { [[ $# -ge 2 ]] && [[ "$2" != --* ]]; } || { echo "ERROR: --fields requires a value (comma-separated field API names), e.g. --fields \"NextStep,Risk__c\""; exit 1; }; SELECTED_FIELDS="$2"; shift 2 ;;
|
|
118
|
+
--field-goal) { [[ $# -ge 2 ]] && [[ "$2" == *:* ]]; } || { echo "ERROR: --field-goal requires \"FieldApiName:goal text\""; exit 1; }; FIELD_PROMPTS["${2%%:*}_goal"]="${2#*:}"; shift 2 ;;
|
|
119
|
+
--field-instruction) { [[ $# -ge 2 ]] && [[ "$2" == *:* ]]; } || { echo "ERROR: --field-instruction requires \"FieldApiName:instruction text\""; exit 1; }; FIELD_PROMPTS["${2%%:*}_instruction"]="${2#*:}"; shift 2 ;;
|
|
120
|
+
--through-phase) { [[ $# -ge 2 ]] && [[ "$2" != --* ]]; } || { echo "ERROR: --through-phase requires a value (currently: prompts)"; exit 1; }; RUN_THROUGH_PHASE="$2"; shift 2 ;;
|
|
121
|
+
--from-phase) { [[ $# -ge 2 ]] && [[ "$2" != --* ]]; } || { echo "ERROR: --from-phase requires a value (currently: flow)"; exit 1; }; RUN_FROM_PHASE="$2"; shift 2 ;;
|
|
122
|
+
*) echo "ERROR: Unknown option: $1"; echo "Usage: $0 <org-alias> [--check-license] [--fields \"NextStep,StageName,Risk__c\"] [--field-goal \"Field:text\"] [--field-instruction \"Field:text\"] [--through-phase prompts] [--from-phase flow] [--autonomous] [--create-stage-descriptions] [--skip-prompt-verification] [--non-interactive|--yes] [--users \"a@x.com,b@x.com\"]"; exit 1 ;;
|
|
123
|
+
esac
|
|
124
|
+
done
|
|
125
|
+
|
|
126
|
+
# --- Validate phase-split flags ---
|
|
127
|
+
# Only "prompts" (--through-phase) and "flow" (--from-phase) are recognized. They
|
|
128
|
+
# are the two ends of the split; passing both, or an unknown name, is rejected so a
|
|
129
|
+
# typo never silently runs the whole thing.
|
|
130
|
+
if [[ -n "$RUN_THROUGH_PHASE" && "$RUN_THROUGH_PHASE" != "prompts" ]]; then
|
|
131
|
+
echo "ERROR: --through-phase supports only 'prompts' (got '$RUN_THROUGH_PHASE')."; exit 1
|
|
132
|
+
fi
|
|
133
|
+
if [[ -n "$RUN_FROM_PHASE" && "$RUN_FROM_PHASE" != "flow" ]]; then
|
|
134
|
+
echo "ERROR: --from-phase supports only 'flow' (got '$RUN_FROM_PHASE')."; exit 1
|
|
135
|
+
fi
|
|
136
|
+
if [[ -n "$RUN_THROUGH_PHASE" && -n "$RUN_FROM_PHASE" ]]; then
|
|
137
|
+
echo "ERROR: --through-phase and --from-phase are mutually exclusive (they are the two halves of a split run)."; exit 1
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
# A non-TTY stdin is treated as non-interactive regardless of flags, so prompts never hang.
|
|
141
|
+
if [[ ! -t 0 ]]; then NON_INTERACTIVE=true; fi
|
|
142
|
+
|
|
143
|
+
# The license preflight asks nothing and mutates nothing — force non-interactive
|
|
144
|
+
# so the Phase 0 Setup Options block (guarded below) is skipped entirely.
|
|
145
|
+
if [[ "$CHECK_LICENSE_ONLY" == "true" ]]; then NON_INTERACTIVE=true; fi
|
|
146
|
+
|
|
147
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
148
|
+
|
|
149
|
+
# Source shared libraries
|
|
150
|
+
source "$SCRIPT_DIR/shared/agent-bundle-publish.sh"
|
|
151
|
+
source "$SCRIPT_DIR/shared/soap.sh"
|
|
152
|
+
source "$SCRIPT_DIR/shared/psg.sh"
|
|
153
|
+
source "$SCRIPT_DIR/shared/flow-builder.sh"
|
|
154
|
+
source "$SCRIPT_DIR/shared/stage-descriptions.sh"
|
|
155
|
+
|
|
156
|
+
trap 'echo ""; echo "Setup interrupted."; exit 130' INT
|
|
157
|
+
|
|
158
|
+
# --- Configuration Constants ---
|
|
159
|
+
readonly API_VERSION="64.0"
|
|
160
|
+
readonly CURL_TIMEOUT=30
|
|
161
|
+
readonly SLEEP_SHORT=2
|
|
162
|
+
readonly SLEEP_MEDIUM=3
|
|
163
|
+
readonly SLEEP_LONG=5
|
|
164
|
+
readonly SLEEP_TOGGLE_OFF=10
|
|
165
|
+
readonly SLEEP_TOGGLE_ON=20
|
|
166
|
+
|
|
167
|
+
# --- Helpers ---
|
|
168
|
+
log_pass() { echo " [PASS] $1"; }
|
|
169
|
+
log_warn() { echo " [WARN] $1"; }
|
|
170
|
+
log_fail() { echo " [FAIL] $1"; }
|
|
171
|
+
log_info() { echo " [....] $1"; }
|
|
172
|
+
log_try() { echo " [TRY ] $1"; }
|
|
173
|
+
section() { echo ""; echo "=== $1 ==="; echo ""; }
|
|
174
|
+
|
|
175
|
+
# with_timeout <seconds> <cmd...> — run a command under a wall-clock bound so a
|
|
176
|
+
# hung network call (e.g. the /generations REST request, which has no built-in
|
|
177
|
+
# --max-time) can't stall the whole run past the harness's tool timeout. Uses
|
|
178
|
+
# coreutils `timeout` (or `gtimeout`) when available; on a stock macOS that has
|
|
179
|
+
# neither, it degrades to running the command unwrapped rather than failing.
|
|
180
|
+
_TIMEOUT_BIN="$(command -v timeout 2>/dev/null || command -v gtimeout 2>/dev/null || true)"
|
|
181
|
+
with_timeout() {
|
|
182
|
+
local secs="$1"; shift
|
|
183
|
+
if [[ -n "$_TIMEOUT_BIN" ]]; then
|
|
184
|
+
"$_TIMEOUT_BIN" "${secs}s" "$@"
|
|
185
|
+
else
|
|
186
|
+
"$@"
|
|
187
|
+
fi
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# confirm <prompt> — yes/no gate. In non-interactive mode returns the SAFE default
|
|
191
|
+
# (decline: return 1) without prompting. Only ever use as `if confirm ...; then`,
|
|
192
|
+
# never as a bare last statement (a nonzero return would trip `set -e`).
|
|
193
|
+
confirm() {
|
|
194
|
+
local prompt="$1"
|
|
195
|
+
if [[ "$NON_INTERACTIVE" == "true" ]]; then
|
|
196
|
+
return 1
|
|
197
|
+
fi
|
|
198
|
+
local reply=""
|
|
199
|
+
read -rp " $prompt [y/N]: " reply || reply=""
|
|
200
|
+
[[ "$reply" =~ ^[Yy] ]]
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# schedule_flow_active <org-alias> — resolve the Pipeline Management suggestion flow
|
|
204
|
+
# using the SAME SourceTemplateId-first resolver as Phase 5 (NOT a fuzzy label match),
|
|
205
|
+
# and print its IsActive state as "true" | "false" | "none" (no flow found). This keeps
|
|
206
|
+
# the summary consistent with detection elsewhere (#6): the old summary read used a
|
|
207
|
+
# label LIKE '%OppSugg%' query that could miss the deployed flow and report it as
|
|
208
|
+
# "Not Found" even right after a successful deploy.
|
|
209
|
+
schedule_flow_active() {
|
|
210
|
+
local org="$1" api_name="" q
|
|
211
|
+
for q in \
|
|
212
|
+
"SELECT ApiName, IsActive FROM FlowDefinitionView WHERE SourceTemplateId='sales_pipe_mgmt__OppSuggGenSchFlow' AND IsTemplate=false" \
|
|
213
|
+
"SELECT ApiName, IsActive FROM FlowDefinitionView WHERE ApiName LIKE '%OppSuggGen%' AND IsTemplate=false" \
|
|
214
|
+
"SELECT ApiName, IsActive FROM FlowDefinitionView WHERE ApiName = 'Process_Field_Update_Suggestions' AND IsTemplate=false"; do
|
|
215
|
+
local rec
|
|
216
|
+
rec=$(sf data query --query "$q" --target-org "$org" --json 2>/dev/null \
|
|
217
|
+
| jq -r 'if (.result.records | length) > 0 then (if .result.records[0].IsActive == true then "true" else "false" end) else empty end' 2>/dev/null || echo "")
|
|
218
|
+
if [[ -n "$rec" ]]; then printf '%s' "$rec"; return 0; fi
|
|
219
|
+
done
|
|
220
|
+
printf 'none'
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# sanitize_dev_name <raw> [prefix] — derive a VALID Salesforce DeveloperName.
|
|
224
|
+
# DeveloperName must be alphanumeric/underscore, start with a letter, and must NOT
|
|
225
|
+
# contain consecutive underscores or a trailing underscore. The naive
|
|
226
|
+
# `tr ' ' '_' | sed 's/\./_/g'` turned "Id. Decision Makers" into "Id__Decision_Makers"
|
|
227
|
+
# (double underscore) which the platform REJECTS — the silent cause behind #8.
|
|
228
|
+
# This collapses runs of underscores, trims leading/trailing underscores, and
|
|
229
|
+
# ensures a leading letter.
|
|
230
|
+
sanitize_dev_name() {
|
|
231
|
+
local raw="$1" prefix="${2:-}" name
|
|
232
|
+
name=$(printf '%s' "${prefix:+${prefix}_}${raw}" \
|
|
233
|
+
| tr ' ' '_' \
|
|
234
|
+
| sed 's/[^A-Za-z0-9_]/_/g' \
|
|
235
|
+
| sed -E 's/_+/_/g' \
|
|
236
|
+
| sed -E 's/^_+//; s/_+$//')
|
|
237
|
+
# DeveloperName must start with a letter.
|
|
238
|
+
if [[ ! "$name" =~ ^[A-Za-z] ]]; then name="X_${name}"; fi
|
|
239
|
+
printf '%s' "$name"
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
# Safe jq extraction - strips CLI warnings, handles parse failures gracefully
|
|
243
|
+
safe_jq() {
|
|
244
|
+
local json="$1" expr="$2" default="${3:-}"
|
|
245
|
+
[[ -z "$json" ]] && { printf '%s' "$default"; return; }
|
|
246
|
+
local cleaned
|
|
247
|
+
# Strip ANSI color escapes (sf --json can emit them into stdout), THEN find JSON start.
|
|
248
|
+
cleaned=$(printf '%s' "$json" | sed -E 's/'$'\x1b''\[[0-9;]*m//g' | sed -n '/^[[:space:]]*[{[]/,$p')
|
|
249
|
+
printf '%s' "$cleaned" | jq -r "$expr" 2>/dev/null || printf '%s' "$default"
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
safe_jq_int() {
|
|
253
|
+
local result
|
|
254
|
+
result=$(safe_jq "$1" "$2" "0")
|
|
255
|
+
[[ "$result" =~ ^[0-9]+$ ]] && echo "$result" || echo "0"
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
# Progress indicators for long-running operations (write to stderr to avoid polluting piped output)
|
|
259
|
+
show_progress() {
|
|
260
|
+
[[ -t 2 ]] || return 0
|
|
261
|
+
local msg="${1:-Working...}"
|
|
262
|
+
printf '\r [....] %s...' "$msg" >&2
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
clear_progress() {
|
|
266
|
+
[[ -t 2 ]] || return 0
|
|
267
|
+
printf '\r\033[K' >&2
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
# Sanitize display strings (remove control chars, truncate)
|
|
271
|
+
sanitize_display() {
|
|
272
|
+
local input="$1" max_len="${2:-80}"
|
|
273
|
+
printf '%s' "$input" | tr -d '\000-\037' | cut -c1-"$max_len"
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
# Sanitize a value for safe SOQL interpolation (alphanumeric, underscores, dots, spaces, slashes)
|
|
277
|
+
sanitize_soql_value() {
|
|
278
|
+
local input="$1"
|
|
279
|
+
printf '%s' "$input" | tr -cd 'A-Za-z0-9_./ -'
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
# redact_token() is provided by shared/soap.sh (sourced above). The shared version
|
|
283
|
+
# fixes session-id tail under-redaction (W-23215519). Do not redefine it here.
|
|
284
|
+
|
|
285
|
+
# Helper: Get license count with error handling
|
|
286
|
+
# Returns: license count or "ERROR" if query fails
|
|
287
|
+
get_license_count() {
|
|
288
|
+
local license_name="$1"
|
|
289
|
+
local result
|
|
290
|
+
result=$(sf data query --query "SELECT TotalLicenses FROM UserLicense WHERE Name = '$license_name'" \
|
|
291
|
+
--target-org "$ORG_ALIAS" --json 2>&1)
|
|
292
|
+
|
|
293
|
+
if echo "$result" | grep -qi "ERROR"; then
|
|
294
|
+
echo "ERROR"
|
|
295
|
+
else
|
|
296
|
+
safe_jq "$result" '.result.records[0].TotalLicenses // 0' "0"
|
|
297
|
+
fi
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
# --- State tracking for before/after summary ---
|
|
301
|
+
declare -A SETUP_STATE_BEFORE
|
|
302
|
+
declare -A SETUP_STATE_AFTER
|
|
303
|
+
declare -A SETUP_CHANGES
|
|
304
|
+
declare -a SETUP_ISSUES=()
|
|
305
|
+
declare -a SETUP_DEPENDENCIES=()
|
|
306
|
+
|
|
307
|
+
track_before() {
|
|
308
|
+
local KEY=$1
|
|
309
|
+
local VALUE=$2
|
|
310
|
+
SETUP_STATE_BEFORE["$KEY"]="$VALUE"
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
track_after() {
|
|
314
|
+
local KEY=$1
|
|
315
|
+
local VALUE=$2
|
|
316
|
+
SETUP_STATE_AFTER["$KEY"]="$VALUE"
|
|
317
|
+
|
|
318
|
+
# Determine change status
|
|
319
|
+
local BEFORE="${SETUP_STATE_BEFORE[$KEY]:-N/A}"
|
|
320
|
+
if [[ "$BEFORE" == "N/A" ]]; then
|
|
321
|
+
SETUP_CHANGES["$KEY"]="🆕 CREATED"
|
|
322
|
+
elif [[ "$BEFORE" != "$VALUE" ]]; then
|
|
323
|
+
SETUP_CHANGES["$KEY"]="✏️ CHANGED"
|
|
324
|
+
else
|
|
325
|
+
SETUP_CHANGES["$KEY"]="✅ NO CHANGE"
|
|
326
|
+
fi
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
track_issue() {
|
|
330
|
+
local SEVERITY=$1
|
|
331
|
+
local DESCRIPTION=$2
|
|
332
|
+
local RESOLUTION=$3
|
|
333
|
+
SETUP_ISSUES+=("$SEVERITY|$DESCRIPTION|$RESOLUTION")
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
track_dependency() {
|
|
337
|
+
local PHASE=$1
|
|
338
|
+
local DEPENDS_ON=$2
|
|
339
|
+
SETUP_DEPENDENCIES+=("$PHASE|$DEPENDS_ON")
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
if [[ -z "$ORG_ALIAS" ]]; then
|
|
344
|
+
echo "Usage: $0 <org-alias> [--autonomous] [--create-stage-descriptions] [--non-interactive|--yes]"
|
|
345
|
+
exit 1
|
|
346
|
+
fi
|
|
347
|
+
|
|
348
|
+
# ============================================================
|
|
349
|
+
# Phase 0: Consent preamble
|
|
350
|
+
# ============================================================
|
|
351
|
+
# Pipeline Management setup is provisioning-heavy. Two behaviours change what the
|
|
352
|
+
# agent does to the org and are therefore gated on explicit consent:
|
|
353
|
+
# 1. Autonomous field updates — the agent auto-applies suggestions with no human review.
|
|
354
|
+
# 2. Stage-description creation — only needed for StageName suggestions; may overwrite
|
|
355
|
+
# or create descriptions the org intentionally left blank.
|
|
356
|
+
# Interactive runs are asked here; non-interactive runs use the SAFE resolved defaults
|
|
357
|
+
# (opt-in only via --autonomous / --create-stage-descriptions).
|
|
358
|
+
# The --check-license preflight skips this block wholesale — it decides nothing
|
|
359
|
+
# about provisioning behaviour and must ask the user nothing.
|
|
360
|
+
if [[ "$CHECK_LICENSE_ONLY" != "true" ]]; then
|
|
361
|
+
section "Phase 0: Setup Options"
|
|
362
|
+
|
|
363
|
+
if [[ "$NON_INTERACTIVE" == "true" ]]; then
|
|
364
|
+
log_info "Non-interactive mode — using resolved options:"
|
|
365
|
+
else
|
|
366
|
+
echo " Before provisioning, confirm two optional behaviours (both default to OFF/safe):"
|
|
367
|
+
echo ""
|
|
368
|
+
# Autonomous updates: opt-in. Flag pre-answers; otherwise ask.
|
|
369
|
+
if [[ "$ENABLE_AUTONOMOUS" != "true" ]]; then
|
|
370
|
+
if confirm "Let the agent update fields AUTONOMOUSLY (auto-apply without human review)? Default is suggestions-only"; then
|
|
371
|
+
ENABLE_AUTONOMOUS=true
|
|
372
|
+
fi
|
|
373
|
+
fi
|
|
374
|
+
# Stage descriptions: opt-in, and only relevant if the user wants StageName suggestions.
|
|
375
|
+
if [[ "$CREATE_STAGE_DESCRIPTIONS" != "true" ]]; then
|
|
376
|
+
if confirm "Create sales-methodology stage descriptions (only needed for StageName suggestions)?"; then
|
|
377
|
+
CREATE_STAGE_DESCRIPTIONS=true
|
|
378
|
+
fi
|
|
379
|
+
fi
|
|
380
|
+
echo ""
|
|
381
|
+
fi
|
|
382
|
+
|
|
383
|
+
echo " • Autonomous field updates: $([[ "$ENABLE_AUTONOMOUS" == "true" ]] && echo "ENABLED (auto-apply)" || echo "off (suggestions only)")"
|
|
384
|
+
echo " • Stage-description creation: $([[ "$CREATE_STAGE_DESCRIPTIONS" == "true" ]] && echo "ENABLED" || echo "off")"
|
|
385
|
+
fi # END Phase 0 Setup Options (skipped under --check-license)
|
|
386
|
+
|
|
387
|
+
# ============================================================
|
|
388
|
+
# Phase 1: Authentication
|
|
389
|
+
# ============================================================
|
|
390
|
+
section "Phase 1: Authentication"
|
|
391
|
+
|
|
392
|
+
# Filter CLI warnings that pollute JSON output (causes jq parse errors)
|
|
393
|
+
# Use buffering pattern to avoid grep eating newlines that jq needs
|
|
394
|
+
ORG_DISPLAY_OUTPUT=$(sf org display --target-org "$ORG_ALIAS" --json 2>&1)
|
|
395
|
+
INSTANCE_URL=$(safe_jq "$ORG_DISPLAY_OUTPUT" '.result.instanceUrl // empty' "")
|
|
396
|
+
if [[ -z "$INSTANCE_URL" ]]; then
|
|
397
|
+
log_fail "Cannot connect to org '$ORG_ALIAS'. Run: sf org login web --alias $ORG_ALIAS"
|
|
398
|
+
exit 1
|
|
399
|
+
fi
|
|
400
|
+
# Enforce HTTPS on instance URL (prevent MITM with access tokens)
|
|
401
|
+
if [[ -n "$INSTANCE_URL" && ! "$INSTANCE_URL" =~ ^https:// ]]; then
|
|
402
|
+
log_fail "Instance URL is not HTTPS: $INSTANCE_URL"
|
|
403
|
+
log_fail "Refusing to send access tokens over insecure connection."
|
|
404
|
+
exit 1
|
|
405
|
+
fi
|
|
406
|
+
|
|
407
|
+
# Extract access token with same warning filtering
|
|
408
|
+
ACCESS_TOKEN=$(safe_jq "$ORG_DISPLAY_OUTPUT" '.result.accessToken // empty' "")
|
|
409
|
+
if [[ -z "$ACCESS_TOKEN" || "$ACCESS_TOKEN" == "null" || "$ACCESS_TOKEN" == *"REDACTED"* ]]; then
|
|
410
|
+
TOKEN_OUTPUT=$(echo "y" | sf org auth show-access-token --target-org "$ORG_ALIAS" --no-prompt --json 2>&1)
|
|
411
|
+
ACCESS_TOKEN=$(safe_jq "$TOKEN_OUTPUT" '.result.accessToken // empty' "")
|
|
412
|
+
fi
|
|
413
|
+
|
|
414
|
+
if [[ -z "$ACCESS_TOKEN" || "$ACCESS_TOKEN" == "null" ]]; then
|
|
415
|
+
log_fail "Cannot extract access token. Re-authenticate: sf org login web --alias $ORG_ALIAS"
|
|
416
|
+
exit 1
|
|
417
|
+
fi
|
|
418
|
+
|
|
419
|
+
log_pass "Connected to $(sanitize_display "$INSTANCE_URL" 80)"
|
|
420
|
+
|
|
421
|
+
# Detect test/trial/scratch orgs and warn about potential API restrictions
|
|
422
|
+
if [[ -n "$INSTANCE_URL" ]]; then
|
|
423
|
+
if [[ "$INSTANCE_URL" =~ (test[0-9]*\.|\.pc-rnd\.|scratch|sandbox|-dev-ed\.|\.trailhead\.|--) ]]; then
|
|
424
|
+
log_warn "TEST/TRIAL ORG DETECTED: $INSTANCE_URL"
|
|
425
|
+
echo ""
|
|
426
|
+
echo " Note: Test, trial, and sandbox orgs may have SOAP API restrictions."
|
|
427
|
+
echo " This script will use alternative approaches (REST API, CLI) where possible."
|
|
428
|
+
echo " Some features may require manual UI enablement if API access is limited."
|
|
429
|
+
echo ""
|
|
430
|
+
fi
|
|
431
|
+
fi
|
|
432
|
+
|
|
433
|
+
# --- API Helpers ---
|
|
434
|
+
# NOTE: These helpers are defined here — right after Phase 1 Authentication — so
|
|
435
|
+
# they are available to the Step 0 capability gate (Phase 0.5, below) that must run
|
|
436
|
+
# BEFORE any interactive question. They depend only on INSTANCE_URL/ACCESS_TOKEN
|
|
437
|
+
# (set in Phase 1), API_VERSION/CURL_TIMEOUT (readonly, top), and
|
|
438
|
+
# show_progress/clear_progress (top-level) — nothing from Phase 1.7 onward. They
|
|
439
|
+
# are also used by BLOCK B (soap_read/soap_extract_bool in the AFTER snapshot,
|
|
440
|
+
# sed_inplace in the flow reconciliation, ensure_sfdx_project before flow/agent
|
|
441
|
+
# deploys); on a --from-phase flow run BLOCK A is skipped entirely, so defining
|
|
442
|
+
# them at this top level keeps them in scope for both blocks and Step 0.
|
|
443
|
+
|
|
444
|
+
# SOAP readMetadata — returns raw XML response
|
|
445
|
+
soap_read() {
|
|
446
|
+
local TYPE="$1" NAME="$2"
|
|
447
|
+
curl -s --max-time $CURL_TIMEOUT "${INSTANCE_URL}/services/Soap/m/${API_VERSION}" \
|
|
448
|
+
-H "Content-Type: text/xml; charset=UTF-8" \
|
|
449
|
+
-H "SOAPAction: readMetadata" \
|
|
450
|
+
-d "<?xml version='1.0' encoding='utf-8'?>
|
|
451
|
+
<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:met='http://soap.sforce.com/2006/04/metadata'>
|
|
452
|
+
<soapenv:Header><met:SessionHeader><met:sessionId>${ACCESS_TOKEN}</met:sessionId></met:SessionHeader></soapenv:Header>
|
|
453
|
+
<soapenv:Body><met:readMetadata><met:type>${TYPE}</met:type><met:fullNames>${NAME}</met:fullNames></met:readMetadata></soapenv:Body>
|
|
454
|
+
</soapenv:Envelope>" 2>/dev/null
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
# Classify a readMetadata response via the shared shared/soap.sh parser.
|
|
458
|
+
# Distinguishes a genuine "false" from an auth/SOAP fault or a network/service
|
|
459
|
+
# error (the W-23215519 fix) instead of collapsing all three to empty.
|
|
460
|
+
# soap_read() does not capture an HTTP status, so "200" is passed as the
|
|
461
|
+
# nominal status; the parser still detects empty bodies, SOAP faults, HTML
|
|
462
|
+
# error pages, and truncated XML from the response content itself.
|
|
463
|
+
# Args: $1 = readMetadata XML, $2 = field name
|
|
464
|
+
# Echoes: "true" | "false" | "AUTH_ERROR:..." | "TYPE_UNAVAILABLE:..." | "NETWORK_ERROR"
|
|
465
|
+
soap_classify() {
|
|
466
|
+
local XML="$1" FIELD="$2"
|
|
467
|
+
parse_soap_response "$XML" "$FIELD" "200"
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
# SOAP updateMetadata — returns raw XML response
|
|
471
|
+
soap_update() {
|
|
472
|
+
local BODY="$1"
|
|
473
|
+
show_progress "SOAP Metadata API call"
|
|
474
|
+
local response
|
|
475
|
+
response=$(curl -s --max-time $CURL_TIMEOUT "${INSTANCE_URL}/services/Soap/m/${API_VERSION}" \
|
|
476
|
+
-H "Content-Type: text/xml; charset=UTF-8" \
|
|
477
|
+
-H "SOAPAction: update" \
|
|
478
|
+
-d "<?xml version='1.0' encoding='utf-8'?>
|
|
479
|
+
<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:met='http://soap.sforce.com/2006/04/metadata'>
|
|
480
|
+
<soapenv:Header><met:SessionHeader><met:sessionId>${ACCESS_TOKEN}</met:sessionId></met:SessionHeader></soapenv:Header>
|
|
481
|
+
<soapenv:Body><met:updateMetadata>${BODY}</met:updateMetadata></soapenv:Body>
|
|
482
|
+
</soapenv:Envelope>" 2>/dev/null)
|
|
483
|
+
clear_progress
|
|
484
|
+
printf '%s' "$response"
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
# Extract a boolean field value from SOAP XML response.
|
|
488
|
+
# Delegates to the shared shared/soap.sh parser for robust extraction (handles
|
|
489
|
+
# truncated XML, HTML error pages, multi-record responses) but preserves this
|
|
490
|
+
# function's original contract — "true", "false", or "" — so the ~15 call sites
|
|
491
|
+
# that only test `== "true"` keep their exact behavior. Auth/type/network
|
|
492
|
+
# classifications collapse to "" here (same as the old grep failure); callers
|
|
493
|
+
# that need to distinguish them use soap_classify directly (see the gate).
|
|
494
|
+
soap_extract_bool() {
|
|
495
|
+
local XML="$1" FIELD="$2"
|
|
496
|
+
local result
|
|
497
|
+
result=$(parse_soap_response "$XML" "$FIELD" "200")
|
|
498
|
+
case "$result" in
|
|
499
|
+
true) echo "true" ;;
|
|
500
|
+
false) echo "false" ;;
|
|
501
|
+
*) echo "" ;;
|
|
502
|
+
esac
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
# Cross-platform sed -i
|
|
506
|
+
sed_inplace() {
|
|
507
|
+
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
508
|
+
sed -i '' "$@"
|
|
509
|
+
else
|
|
510
|
+
sed -i "$@"
|
|
511
|
+
fi
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
# Ensure sfdx-project.json exists for CLI operations
|
|
515
|
+
ensure_sfdx_project() {
|
|
516
|
+
if [[ ! -f "sfdx-project.json" ]]; then
|
|
517
|
+
cat > sfdx-project.json << 'SFDX'
|
|
518
|
+
{"packageDirectories": [{"path": "force-app", "default": true}], "sourceApiVersion": "67.0"}
|
|
519
|
+
SFDX
|
|
520
|
+
fi
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
# Print the "org can't enable Pipeline Management" blocker body (license list,
|
|
524
|
+
# edition guidance, Setup URL). Shared by the Step 0 gate (Phase 0.5) and the
|
|
525
|
+
# Phase 2.5 backstop so the message is authored once. Callers own the log_fail
|
|
526
|
+
# banner and the exit; this only prints the explanatory body.
|
|
527
|
+
# Args: $1 = Einstein Agent license count, $2 = Agentforce-for-Sales license count
|
|
528
|
+
# (either may be a number, "unknown", or empty)
|
|
529
|
+
print_license_blocker() {
|
|
530
|
+
local ea_count="${1:-unknown}" sales_count="${2:-unknown}"
|
|
531
|
+
echo ""
|
|
532
|
+
echo " BLOCKER: This org cannot enable Pipeline Management programmatically."
|
|
533
|
+
echo ""
|
|
534
|
+
echo " Reason: The SalesDealAgentSettings metadata type is not provisioned."
|
|
535
|
+
echo " This typically means the org is missing required licenses."
|
|
536
|
+
echo ""
|
|
537
|
+
echo " Current org licenses:"
|
|
538
|
+
echo " • Einstein Agent licenses: $ea_count"
|
|
539
|
+
echo " • Agentforce for Sales licenses: $sales_count"
|
|
540
|
+
echo ""
|
|
541
|
+
echo " Pipeline Management requires:"
|
|
542
|
+
echo " ✓ Agentforce for Sales add-on license, OR"
|
|
543
|
+
echo " ✓ Sales Cloud with Einstein capabilities, OR"
|
|
544
|
+
echo " ✓ Agentforce 1 Sales Edition"
|
|
545
|
+
echo ""
|
|
546
|
+
echo " Actions:"
|
|
547
|
+
echo " 1. Contact your Salesforce account team to provision Agentforce for Sales licenses"
|
|
548
|
+
echo " 2. Verify org edition supports Pipeline Management (Enterprise Edition or higher)"
|
|
549
|
+
echo " 3. Try manual enablement via Setup UI (if available):"
|
|
550
|
+
echo " ${INSTANCE_URL}/lightning/setup/PipelineManagement/home"
|
|
551
|
+
echo " (Note: Assumes Lightning Experience. For Classic, use Setup menu.)"
|
|
552
|
+
echo ""
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
# ============================================================
|
|
556
|
+
# Phase 0.5: License & Capability Gate (Step 0)
|
|
557
|
+
# ============================================================
|
|
558
|
+
# Block an ineligible org UP FRONT — before any interactive question (Phase 1.7
|
|
559
|
+
# field selection) or mutation. The authoritative signal is the same one the
|
|
560
|
+
# Phase 2.5 backstop uses: probe the SalesDealAgentSettings metadata type over
|
|
561
|
+
# SOAP and hard-block ONLY on INVALID_TYPE (soap_classify => TYPE_UNAVAILABLE).
|
|
562
|
+
# License/edition COUNTS are informational context, never a block, because
|
|
563
|
+
# UserLicense can read 0 while PM still works via base Einstein Agent
|
|
564
|
+
# entitlements. An already-provisioned org has the type (probe => true/false),
|
|
565
|
+
# so it passes and idempotent re-runs stay green. Runs on BOTH the single-shot
|
|
566
|
+
# and --from-phase flow paths — it's read-only and fast.
|
|
567
|
+
section "Phase 0.5: License & Capability Gate"
|
|
568
|
+
|
|
569
|
+
STEP0_CAP=$(soap_classify "$(soap_read "SalesDealAgentSettings" "SalesDealAgent")" "enableDealAgent")
|
|
570
|
+
|
|
571
|
+
# Informational context (non-blocking): edition + license counts.
|
|
572
|
+
STEP0_EDITION=$(sf data query --query "SELECT OrganizationType FROM Organization" --target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.records[0].OrganizationType // "unknown"' 2>/dev/null || echo "unknown")
|
|
573
|
+
STEP0_EA_COUNT=$(get_license_count "Einstein Agent")
|
|
574
|
+
[[ "$STEP0_EA_COUNT" == "ERROR" ]] && STEP0_EA_COUNT="unknown"
|
|
575
|
+
STEP0_SALES_COUNT=$(safe_jq_int "$(sf data query --query "SELECT COUNT(Id) FROM UserLicense WHERE (Name LIKE '%Agentforce for Sales%' OR Name LIKE '%Sales Cloud Einstein%' OR Name LIKE '%Agentforce 1%') AND TotalLicenses > 0" --target-org "$ORG_ALIAS" --json 2>/dev/null)" '.result.records[0].expr0 // 0')
|
|
576
|
+
|
|
577
|
+
echo " Org edition: $STEP0_EDITION"
|
|
578
|
+
echo " Einstein Agent licenses: $STEP0_EA_COUNT"
|
|
579
|
+
echo " Agentforce for Sales / Sales Cloud Einstein / Agentforce 1 licenses: $STEP0_SALES_COUNT"
|
|
580
|
+
echo ""
|
|
581
|
+
|
|
582
|
+
case "$STEP0_CAP" in
|
|
583
|
+
true|false)
|
|
584
|
+
log_pass "GATE: Pipeline Management capability confirmed (SalesDealAgentSettings available) ✓"
|
|
585
|
+
;;
|
|
586
|
+
TYPE_UNAVAILABLE:*)
|
|
587
|
+
log_fail "GATE: This org is not licensed for Pipeline Management"
|
|
588
|
+
print_license_blocker "$STEP0_EA_COUNT" "$STEP0_SALES_COUNT"
|
|
589
|
+
echo " Stopping now — before any configuration questions — so nothing is changed on an org that cannot run Pipeline Management."
|
|
590
|
+
echo ""
|
|
591
|
+
exit 1
|
|
592
|
+
;;
|
|
593
|
+
AUTH_ERROR:*)
|
|
594
|
+
log_warn "GATE: Couldn't confirm capability over SOAP yet (SOAP API access issue)."
|
|
595
|
+
echo " This is a separate, fixable prerequisite — it's re-checked and explained"
|
|
596
|
+
echo " at the SOAP prerequisites step (Phase 1.5). Proceeding."
|
|
597
|
+
echo ""
|
|
598
|
+
;;
|
|
599
|
+
NETWORK_ERROR|*)
|
|
600
|
+
log_warn "GATE: Capability probe inconclusive (transient network/service error). Proceeding; the Phase 2.5 backstop will re-check."
|
|
601
|
+
echo ""
|
|
602
|
+
;;
|
|
603
|
+
esac
|
|
604
|
+
|
|
605
|
+
# Non-blocking permission reminder — scripts cannot check these; the executing
|
|
606
|
+
# user must hold them for setup to complete (see SKILL.md "You must verify").
|
|
607
|
+
echo " Reminder — the executing user needs these permissions (not script-checkable):"
|
|
608
|
+
echo " • View Setup AND (Modify All Data OR Customize Application)"
|
|
609
|
+
echo " • Manage AI Agents AND (Manage Agentforce Employee Agents OR Customize Application)"
|
|
610
|
+
echo " • Assign Permission Sets"
|
|
611
|
+
echo ""
|
|
612
|
+
|
|
613
|
+
# --- License preflight stops here (--check-license) ---
|
|
614
|
+
# We've run auth + the Phase 0.5 gate. If the org were unlicensed the gate would
|
|
615
|
+
# already have printed the blocker and exit 1'd; reaching here means it's capable
|
|
616
|
+
# (or the probe was inconclusive — a non-blocking AUTH/NETWORK warning). Exit 0 so
|
|
617
|
+
# the driving agent can proceed to ask its clarifying questions. No mutation ran.
|
|
618
|
+
if [[ "$CHECK_LICENSE_ONLY" == "true" ]]; then
|
|
619
|
+
log_pass "Preflight passed — org is capable of Pipeline Management. No changes made."
|
|
620
|
+
echo " Proceed to configuration (field selection, prompt tuning, then setup)."
|
|
621
|
+
echo ""
|
|
622
|
+
exit 0
|
|
623
|
+
fi
|
|
624
|
+
|
|
625
|
+
# ============================================================
|
|
626
|
+
# Phase 1.7: Field Selection (up front, before any provisioning)
|
|
627
|
+
# ============================================================
|
|
628
|
+
# 3-phase setup, part 1 of the "field" story: decide WHICH Opportunity fields the
|
|
629
|
+
# suggestion flow will manage BEFORE we build/deploy the flow, so the flow is built
|
|
630
|
+
# with only those fields (no deploy-then-strip). Done here — right after auth,
|
|
631
|
+
# before the heavy provisioning phases — so an empty/oversized selection fails fast
|
|
632
|
+
# rather than after minutes of enablement work.
|
|
633
|
+
#
|
|
634
|
+
# Rules:
|
|
635
|
+
# - At least 1 field REQUIRED; setup ABORTS on an empty selection (no default).
|
|
636
|
+
# - Hard cap MAX_SUGGESTION_FIELDS (OOTB NextStep/StageName count toward it).
|
|
637
|
+
# - StageName is a picklist: NOT prompt-customizable, and it needs stage
|
|
638
|
+
# descriptions (Phase 6) — flagged via STAGENAME_SELECTED.
|
|
639
|
+
# - Per non-StageName field we collect an optional goal/instruction for the
|
|
640
|
+
# prompt template (NextStep -> OOTB override; custom fields -> new template).
|
|
641
|
+
section "Phase 1.7: Field Selection"
|
|
642
|
+
|
|
643
|
+
if [[ -z "$SELECTED_FIELDS" ]]; then
|
|
644
|
+
if [[ "$NON_INTERACTIVE" == "true" ]]; then
|
|
645
|
+
log_fail "No fields selected. Pipeline Management needs at least one field to manage."
|
|
646
|
+
echo " Pass --fields with a comma-separated list of Opportunity field API names."
|
|
647
|
+
echo " OOTB fields: NextStep (Next Step), StageName (Opportunity Stage)."
|
|
648
|
+
echo " Example: $0 $ORG_ALIAS --fields \"NextStep\" --non-interactive"
|
|
649
|
+
exit 1
|
|
650
|
+
fi
|
|
651
|
+
echo "Which Opportunity fields should the agent generate suggestions for?"
|
|
652
|
+
echo " • NextStep — Next Step (OOTB; works out of the box)"
|
|
653
|
+
echo " • StageName — Opportunity Stage (OOTB; requires stage descriptions)"
|
|
654
|
+
echo " • <API name> — any eligible custom text field (Text or Text Area <=255)"
|
|
655
|
+
echo ""
|
|
656
|
+
echo " Enter a comma-separated list (at least 1, at most $MAX_SUGGESTION_FIELDS). Setup aborts if empty."
|
|
657
|
+
read -rp " Fields: " SELECTED_FIELDS || SELECTED_FIELDS=""
|
|
658
|
+
fi
|
|
659
|
+
|
|
660
|
+
# Normalize: trim whitespace around each entry, drop empties, de-dup (order-preserving).
|
|
661
|
+
declare -a FIELD_ARRAY=()
|
|
662
|
+
IFS=',' read -ra _raw_fields <<< "$SELECTED_FIELDS"
|
|
663
|
+
for _f in "${_raw_fields[@]}"; do
|
|
664
|
+
_f="$(echo "$_f" | xargs)" # trim
|
|
665
|
+
[[ -z "$_f" ]] && continue
|
|
666
|
+
_dup=false
|
|
667
|
+
for _seen in "${FIELD_ARRAY[@]:-}"; do [[ "$_seen" == "$_f" ]] && { _dup=true; break; }; done
|
|
668
|
+
[[ "$_dup" == "true" ]] || FIELD_ARRAY+=("$_f")
|
|
669
|
+
done
|
|
670
|
+
|
|
671
|
+
if [[ ${#FIELD_ARRAY[@]} -eq 0 ]]; then
|
|
672
|
+
log_fail "No fields selected. Setup aborted — Pipeline Management requires at least one field."
|
|
673
|
+
echo " Re-run and select at least one field (e.g., NextStep)."
|
|
674
|
+
exit 1
|
|
675
|
+
fi
|
|
676
|
+
if [[ ${#FIELD_ARRAY[@]} -gt $MAX_SUGGESTION_FIELDS ]]; then
|
|
677
|
+
log_fail "Too many fields selected (${#FIELD_ARRAY[@]}). Maximum is $MAX_SUGGESTION_FIELDS (OOTB fields count toward the cap)."
|
|
678
|
+
exit 1
|
|
679
|
+
fi
|
|
680
|
+
|
|
681
|
+
# Rebuild the canonical comma list from the normalized array so downstream
|
|
682
|
+
# consumers (flow builder, summary) see the de-duped, trimmed set.
|
|
683
|
+
SELECTED_FIELDS="$(IFS=,; echo "${FIELD_ARRAY[*]}")"
|
|
684
|
+
|
|
685
|
+
STAGENAME_SELECTED=false
|
|
686
|
+
for _f in "${FIELD_ARRAY[@]}"; do [[ "$_f" == "StageName" ]] && STAGENAME_SELECTED=true; done
|
|
687
|
+
|
|
688
|
+
log_pass "Fields selected (${#FIELD_ARRAY[@]}): $SELECTED_FIELDS"
|
|
689
|
+
[[ "$STAGENAME_SELECTED" == "true" ]] && log_info "StageName selected — stage descriptions will be needed (Phase 6)."
|
|
690
|
+
|
|
691
|
+
# Collect prompt customization for each non-StageName field. StageName is a
|
|
692
|
+
# picklist and is never customizable. Interactive only — in non-interactive mode
|
|
693
|
+
# the goal/instruction come from --field-goal / --field-instruction (or defaults
|
|
694
|
+
# inside add-field-suggestion.sh). NextStep with no customization stays on its
|
|
695
|
+
# curated OOTB managed prompt.
|
|
696
|
+
if [[ "$NON_INTERACTIVE" != "true" ]]; then
|
|
697
|
+
for _f in "${FIELD_ARRAY[@]}"; do
|
|
698
|
+
[[ "$_f" == "StageName" ]] && continue
|
|
699
|
+
if [[ "$_f" == "NextStep" ]]; then
|
|
700
|
+
confirm "Customize the AI prompt for NextStep? (N = keep the OOTB prompt)" || continue
|
|
701
|
+
else
|
|
702
|
+
echo ""
|
|
703
|
+
echo "Configure the AI prompt for custom field '$_f':"
|
|
704
|
+
fi
|
|
705
|
+
read -rp " Goal (completes 'You must think about ...'): " _goal || _goal=""
|
|
706
|
+
read -rp " Instruction (one line of extraction guidance): " _instr || _instr=""
|
|
707
|
+
[[ -n "$_goal" ]] && FIELD_PROMPTS["${_f}_goal"]="$_goal"
|
|
708
|
+
[[ -n "$_instr" ]] && FIELD_PROMPTS["${_f}_instruction"]="$_instr"
|
|
709
|
+
done
|
|
710
|
+
fi
|
|
711
|
+
echo ""
|
|
712
|
+
|
|
713
|
+
# ============================================================
|
|
714
|
+
# Pre-Block-A: Snapshot org state for accurate Configuration Summary
|
|
715
|
+
# (W-23277501 — status column must reflect before-vs-after, not hardcoded
|
|
716
|
+
# greenfield literals, so idempotent reruns report UNCHANGED)
|
|
717
|
+
# ============================================================
|
|
718
|
+
# This is READ-ONLY org state and MUST run on BOTH the single-shot and the
|
|
719
|
+
# --from-phase flow paths, so it lives ABOVE the Block A `if`. It was previously
|
|
720
|
+
# inside Block A, which left BEFORE_* unbound on a --from-phase flow run and
|
|
721
|
+
# crashed the Phase 8 before/after table under `set -u`. All helpers it calls
|
|
722
|
+
# (soap_read, soap_extract_bool, schedule_flow_active, sanitize_display) are
|
|
723
|
+
# top-level, so this is safe here.
|
|
724
|
+
# Cache one SOAP read so both settings are extracted from the same document
|
|
725
|
+
SOAP_SETTINGS_BEFORE=$(soap_read "SalesDealAgentSettings" "SalesDealAgent" 2>/dev/null || echo "")
|
|
726
|
+
BEFORE_PM=$(soap_extract_bool "$SOAP_SETTINGS_BEFORE" "enableDealAgent" 2>/dev/null || echo "false")
|
|
727
|
+
BEFORE_AUTO=$(soap_extract_bool "$SOAP_SETTINGS_BEFORE" "enableDealAgentAutoApproveAllTasks" 2>/dev/null || echo "false")
|
|
728
|
+
BEFORE_AGENT_USER=$(sf data query --query "SELECT Username FROM User WHERE Username LIKE '%salesmanagementagentuser%'" --target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.records[0].Username // "Not Created"' 2>/dev/null || echo "Not Created")
|
|
729
|
+
BEFORE_AGENT_USER=$(sanitize_display "$BEFORE_AGENT_USER" 60)
|
|
730
|
+
BEFORE_PSG_COUNT=$(sf data query --query "SELECT Id FROM PermissionSetGroup WHERE DeveloperName IN ('SalesManagementUserPsg','SalesManagementAgentUserPsg')" --target-org "$ORG_ALIAS" --use-tooling-api --json 2>/dev/null | jq -r '.result.totalSize // 0' 2>/dev/null || echo "0")
|
|
731
|
+
BEFORE_PSG_COUNT=$((${BEFORE_PSG_COUNT:-0} + 0))
|
|
732
|
+
# Normalize jq boolean output — treat literal null / missing / non-true as "none"
|
|
733
|
+
BEFORE_FLOW_ACTIVE=$(schedule_flow_active "$ORG_ALIAS")
|
|
734
|
+
# OpptStageDescription does not exist until Pipeline Management is enabled, so on
|
|
735
|
+
# a greenfield org this query fails with INVALID_TYPE. Under `set -o pipefail` the
|
|
736
|
+
# failed pipeline leaves jq's "0" on stdout AND (with a trailing `|| echo "0"`)
|
|
737
|
+
# would append a second "0", yielding a two-line value that crashes the arithmetic
|
|
738
|
+
# below with "0\n0". Swallow the failure with `|| true` and let jq's `// 0` supply
|
|
739
|
+
# the default — this preserves the BEFORE snapshot (W-23277501) on greenfield orgs.
|
|
740
|
+
BEFORE_STAGE_DESC_COUNT=$(sf data query --query "SELECT Id FROM OpptStageDescription" --target-org "$ORG_ALIAS" --use-tooling-api --json 2>/dev/null | jq -r '.result.totalSize // 0' 2>/dev/null || true)
|
|
741
|
+
BEFORE_STAGE_DESC_COUNT=$((${BEFORE_STAGE_DESC_COUNT:-0} + 0))
|
|
742
|
+
|
|
743
|
+
# ============================================================
|
|
744
|
+
# BLOCK A — Provisioning & Prompts (Phase 1.5 → 4e)
|
|
745
|
+
# ============================================================
|
|
746
|
+
# Everything from SOAP prereqs through prompt-template deploy + the verification
|
|
747
|
+
# gate. On a --from-phase flow run this entire block is SKIPPED: the org was
|
|
748
|
+
# already enabled/provisioned and the templates deployed+tuned in the prior
|
|
749
|
+
# --through-phase prompts call, so we jump straight to building the flow (Phase 5)
|
|
750
|
+
# from the agent-approved --fields set. The context phases above (0/1/1.7) always
|
|
751
|
+
# run, so SELECTED_FIELDS / FIELD_ARRAY / STAGENAME_SELECTED are populated either way.
|
|
752
|
+
if [[ "$RUN_FROM_PHASE" == "flow" ]]; then
|
|
753
|
+
section "Provisioning & Prompts (Phase 1.5–4e)"
|
|
754
|
+
log_info "Skipped (--from-phase flow): org already enabled and prompt templates deployed in the prior --through-phase prompts run."
|
|
755
|
+
log_info "Building the flow from the approved field set: $SELECTED_FIELDS"
|
|
756
|
+
else
|
|
757
|
+
|
|
758
|
+
# ============================================================
|
|
759
|
+
# Phase 1.5: SOAP API Prerequisites Verification
|
|
760
|
+
# ============================================================
|
|
761
|
+
section "Phase 1.5: SOAP API Prerequisites"
|
|
762
|
+
|
|
763
|
+
echo "This script requires SOAP API access to enable Pipeline Management settings."
|
|
764
|
+
echo "Testing SOAP API connectivity..."
|
|
765
|
+
echo ""
|
|
766
|
+
|
|
767
|
+
# Test SOAP API with a simple read operation
|
|
768
|
+
show_progress "Testing SOAP API connectivity"
|
|
769
|
+
SOAP_TEST=$(curl -s --max-time $CURL_TIMEOUT "${INSTANCE_URL}/services/Soap/m/${API_VERSION}" \
|
|
770
|
+
-H "Content-Type: text/xml; charset=UTF-8" \
|
|
771
|
+
-H "SOAPAction: readMetadata" \
|
|
772
|
+
-d "<?xml version='1.0' encoding='utf-8'?>
|
|
773
|
+
<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:met='http://soap.sforce.com/2006/04/metadata'>
|
|
774
|
+
<soapenv:Header><met:SessionHeader><met:sessionId>${ACCESS_TOKEN}</met:sessionId></met:SessionHeader></soapenv:Header>
|
|
775
|
+
<soapenv:Body><met:readMetadata><met:type>EinsteinGptSettings</met:type><met:fullNames>EinsteinGpt</met:fullNames></met:readMetadata></soapenv:Body>
|
|
776
|
+
</soapenv:Envelope>" 2>&1)
|
|
777
|
+
|
|
778
|
+
clear_progress
|
|
779
|
+
|
|
780
|
+
# Check for INVALID_LOGIN error (most common SOAP access issue)
|
|
781
|
+
if echo "$SOAP_TEST" | grep -q "INVALID_LOGIN"; then
|
|
782
|
+
log_fail "SOAP API login failed"
|
|
783
|
+
echo ""
|
|
784
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
785
|
+
echo " BLOCKER: SOAP API Access Not Enabled"
|
|
786
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
787
|
+
echo ""
|
|
788
|
+
echo "This script uses the SOAP Metadata API to enable Pipeline Management"
|
|
789
|
+
echo "settings. Your org or user account does not have SOAP API access enabled."
|
|
790
|
+
echo ""
|
|
791
|
+
echo "Error received: INVALID_LOGIN"
|
|
792
|
+
echo ""
|
|
793
|
+
echo "SOAP API requires two prerequisites:"
|
|
794
|
+
echo ""
|
|
795
|
+
echo "1️⃣ ORG-LEVEL SETTING: Enable SOAP API Login"
|
|
796
|
+
echo ""
|
|
797
|
+
echo " a. Navigate to: ${INSTANCE_URL}/lightning/setup/UserInterfaceUI/home"
|
|
798
|
+
echo " OR: Setup → Quick Find: 'User Interface'"
|
|
799
|
+
echo ""
|
|
800
|
+
echo " b. Check the box: ☑ Enable SOAP API login()"
|
|
801
|
+
echo ""
|
|
802
|
+
echo " c. Click Save"
|
|
803
|
+
echo ""
|
|
804
|
+
echo "2️⃣ USER-LEVEL PERMISSION: Grant 'Use Any API Auth' Permission"
|
|
805
|
+
echo ""
|
|
806
|
+
echo " Option A: Via Permission Set (Recommended)"
|
|
807
|
+
echo " ─────────────────────────────────────────"
|
|
808
|
+
echo " a. Navigate to: ${INSTANCE_URL}/lightning/setup/PermSets/home"
|
|
809
|
+
echo " OR: Setup → Quick Find: 'Permission Sets'"
|
|
810
|
+
echo ""
|
|
811
|
+
echo " b. Create new Permission Set (e.g., 'API Access')"
|
|
812
|
+
echo ""
|
|
813
|
+
echo " c. Click 'System Permissions' → Edit"
|
|
814
|
+
echo ""
|
|
815
|
+
echo " d. Enable: ☑ Use Any API Auth"
|
|
816
|
+
echo ""
|
|
817
|
+
echo " e. Save → Manage Assignments → Assign to your user"
|
|
818
|
+
echo ""
|
|
819
|
+
echo " Option B: Via Profile (Alternative)"
|
|
820
|
+
echo " ────────────────────────────────────"
|
|
821
|
+
echo " a. Navigate to your user profile settings"
|
|
822
|
+
echo ""
|
|
823
|
+
echo " b. Edit → System Permissions"
|
|
824
|
+
echo ""
|
|
825
|
+
echo " c. Enable: ☑ Use Any API Auth"
|
|
826
|
+
echo ""
|
|
827
|
+
echo " d. Save"
|
|
828
|
+
echo ""
|
|
829
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
830
|
+
echo ""
|
|
831
|
+
echo "After enabling both prerequisites, re-run this script:"
|
|
832
|
+
echo " bash setup-all.sh $ORG_ALIAS"
|
|
833
|
+
echo ""
|
|
834
|
+
exit 1
|
|
835
|
+
fi
|
|
836
|
+
|
|
837
|
+
# Check for other SOAP errors
|
|
838
|
+
if echo "$SOAP_TEST" | grep -q "<faultcode>"; then
|
|
839
|
+
FAULT_CODE=$(echo "$SOAP_TEST" | grep -o "<faultcode>[^<]*" | sed 's/<faultcode>//')
|
|
840
|
+
FAULT_STRING=$(redact_token "$(echo "$SOAP_TEST" | grep -o "<faultstring>[^<]*" | sed 's/<faultstring>//')")
|
|
841
|
+
|
|
842
|
+
log_warn "SOAP API test returned an error"
|
|
843
|
+
echo ""
|
|
844
|
+
echo " Fault Code: $FAULT_CODE"
|
|
845
|
+
echo " Fault Message: $FAULT_STRING"
|
|
846
|
+
echo ""
|
|
847
|
+
|
|
848
|
+
if [[ "$FAULT_CODE" == *"INVALID_TYPE"* ]]; then
|
|
849
|
+
log_info "This may be expected for test/trial orgs. Proceeding..."
|
|
850
|
+
track_issue "⚠️ WARN" "SOAP API returned INVALID_TYPE" "Expected for test/trial orgs - Continued"
|
|
851
|
+
echo ""
|
|
852
|
+
else
|
|
853
|
+
echo " This may indicate a SOAP API configuration issue."
|
|
854
|
+
echo " If setup fails, verify SOAP API prerequisites above."
|
|
855
|
+
track_issue "⚠️ WARN" "SOAP API error: $FAULT_CODE" "User confirmed continue"
|
|
856
|
+
echo ""
|
|
857
|
+
# Non-interactive: skip (don't block on user input in agent context)
|
|
858
|
+
if [[ -t 0 ]]; then
|
|
859
|
+
read -p "Press Enter to continue anyway, or Ctrl+C to exit..."
|
|
860
|
+
fi
|
|
861
|
+
echo ""
|
|
862
|
+
fi
|
|
863
|
+
else
|
|
864
|
+
log_pass "SOAP API access verified ✓"
|
|
865
|
+
fi
|
|
866
|
+
|
|
867
|
+
# --- Generic enable-and-verify harness ---
|
|
868
|
+
# Tries SOAP → CLI deploy → verifies via SOAP readMetadata
|
|
869
|
+
# Returns:
|
|
870
|
+
# 0 = success
|
|
871
|
+
# 1 = failure (all approaches exhausted)
|
|
872
|
+
# 2 = metadata type not available (INVALID_TYPE - org lacks license/edition)
|
|
873
|
+
#
|
|
874
|
+
# Usage: enable_setting <MetadataType> <fullName> <field> <displayName>
|
|
875
|
+
enable_setting() {
|
|
876
|
+
local META_TYPE="$1"
|
|
877
|
+
local FULL_NAME="$2"
|
|
878
|
+
local FIELD="$3"
|
|
879
|
+
local DISPLAY_NAME="$4"
|
|
880
|
+
|
|
881
|
+
# --- CHECK: already enabled? ---
|
|
882
|
+
local CURRENT
|
|
883
|
+
local READ_RESULT
|
|
884
|
+
READ_RESULT=$(soap_read "$META_TYPE" "$FULL_NAME")
|
|
885
|
+
|
|
886
|
+
# Check for INVALID_TYPE in read response (metadata type not available)
|
|
887
|
+
if echo "$READ_RESULT" | grep -qi "INVALID_TYPE"; then
|
|
888
|
+
log_warn "$DISPLAY_NAME: metadata type not available in this org (licensing/edition limitation)"
|
|
889
|
+
return 2
|
|
890
|
+
fi
|
|
891
|
+
|
|
892
|
+
CURRENT=$(soap_extract_bool "$READ_RESULT" "$FIELD")
|
|
893
|
+
if [[ "$CURRENT" == "true" ]]; then
|
|
894
|
+
log_pass "$DISPLAY_NAME: enabled"
|
|
895
|
+
return 0
|
|
896
|
+
fi
|
|
897
|
+
|
|
898
|
+
# --- APPROACH 1: SOAP updateMetadata ---
|
|
899
|
+
log_try "$DISPLAY_NAME: enabling via SOAP Metadata API..."
|
|
900
|
+
local SOAP_RESULT
|
|
901
|
+
SOAP_RESULT=$(soap_update "<met:metadata xsi:type='met:${META_TYPE}' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'><met:fullName>${FULL_NAME}</met:fullName><met:${FIELD}>true</met:${FIELD}></met:metadata>")
|
|
902
|
+
|
|
903
|
+
# Check for INVALID_TYPE in update response
|
|
904
|
+
if echo "$SOAP_RESULT" | grep -qi "INVALID_TYPE"; then
|
|
905
|
+
log_warn "$DISPLAY_NAME: metadata type not available in this org (licensing/edition limitation)"
|
|
906
|
+
return 2
|
|
907
|
+
fi
|
|
908
|
+
|
|
909
|
+
if echo "$SOAP_RESULT" | grep -q "<success>true</success>"; then
|
|
910
|
+
sleep $SLEEP_SHORT
|
|
911
|
+
# VERIFY
|
|
912
|
+
CURRENT=$(soap_extract_bool "$(soap_read "$META_TYPE" "$FULL_NAME")" "$FIELD")
|
|
913
|
+
if [[ "$CURRENT" == "true" ]]; then
|
|
914
|
+
log_pass "$DISPLAY_NAME: enabled (SOAP)"
|
|
915
|
+
return 0
|
|
916
|
+
fi
|
|
917
|
+
log_warn "$DISPLAY_NAME: SOAP reported success but verification failed. Retrying..."
|
|
918
|
+
sleep $SLEEP_LONG
|
|
919
|
+
CURRENT=$(soap_extract_bool "$(soap_read "$META_TYPE" "$FULL_NAME")" "$FIELD")
|
|
920
|
+
if [[ "$CURRENT" == "true" ]]; then
|
|
921
|
+
log_pass "$DISPLAY_NAME: enabled (SOAP, delayed verification)"
|
|
922
|
+
return 0
|
|
923
|
+
fi
|
|
924
|
+
fi
|
|
925
|
+
|
|
926
|
+
local SOAP_ERR
|
|
927
|
+
SOAP_ERR=$(echo "$SOAP_RESULT" | grep -o '<message>[^<]*</message>' | sed 's/<[^>]*>//g' | head -1 || echo "")
|
|
928
|
+
[[ -n "$SOAP_ERR" ]] && echo " SOAP error: $(redact_token "$SOAP_ERR")"
|
|
929
|
+
|
|
930
|
+
# --- APPROACH 2: CLI retrieve → modify → deploy ---
|
|
931
|
+
# Map MetadataType to CLI settings name (strip "Settings" suffix for CLI)
|
|
932
|
+
local CLI_NAME="${META_TYPE%Settings}"
|
|
933
|
+
# Special cases where CLI name differs
|
|
934
|
+
case "$META_TYPE" in
|
|
935
|
+
EinsteinGptSettings) CLI_NAME="EinsteinGpt" ;;
|
|
936
|
+
EinsteinCopilotSettings) CLI_NAME="EinsteinCopilot" ;;
|
|
937
|
+
AgentPlatformSettings) CLI_NAME="AgentPlatform" ;;
|
|
938
|
+
EnhancedNotesSettings) CLI_NAME="EnhancedNotes" ;;
|
|
939
|
+
OpportunitySettings) CLI_NAME="Opportunity" ;;
|
|
940
|
+
SalesDealAgentSettings) CLI_NAME="SalesDealAgent" ;;
|
|
941
|
+
esac
|
|
942
|
+
|
|
943
|
+
log_try "$DISPLAY_NAME: trying CLI retrieve/modify/deploy..."
|
|
944
|
+
ensure_sfdx_project
|
|
945
|
+
mkdir -p force-app/main/default/settings
|
|
946
|
+
|
|
947
|
+
# Attempt retrieve
|
|
948
|
+
sf project retrieve start --metadata "Settings:${CLI_NAME}" --target-org "$ORG_ALIAS" --json 2>/dev/null >/dev/null || true
|
|
949
|
+
|
|
950
|
+
local SETTINGS_FILE="force-app/main/default/settings/${CLI_NAME}.settings-meta.xml"
|
|
951
|
+
if [[ -f "$SETTINGS_FILE" ]]; then
|
|
952
|
+
# Modify: update existing field or insert new field
|
|
953
|
+
if grep -q "<${FIELD}>" "$SETTINGS_FILE"; then
|
|
954
|
+
sed_inplace "s|<${FIELD}>[^<]*</${FIELD}>|<${FIELD}>true</${FIELD}>|g" "$SETTINGS_FILE"
|
|
955
|
+
else
|
|
956
|
+
# Insert before closing tag
|
|
957
|
+
sed_inplace "/<\/.*Settings>/i\\
|
|
958
|
+
<${FIELD}>true</${FIELD}>" "$SETTINGS_FILE"
|
|
959
|
+
fi
|
|
960
|
+
|
|
961
|
+
# Deploy
|
|
962
|
+
local DEPLOY_RESULT
|
|
963
|
+
DEPLOY_RESULT=$(sf project deploy start --source-dir force-app/main/default/settings --target-org "$ORG_ALIAS" --json 2>/dev/null || echo '{"status":1}')
|
|
964
|
+
|
|
965
|
+
if echo "$DEPLOY_RESULT" | jq -e '.status == 0' >/dev/null 2>&1; then
|
|
966
|
+
sleep $SLEEP_MEDIUM
|
|
967
|
+
# VERIFY
|
|
968
|
+
CURRENT=$(soap_extract_bool "$(soap_read "$META_TYPE" "$FULL_NAME")" "$FIELD")
|
|
969
|
+
if [[ "$CURRENT" == "true" ]]; then
|
|
970
|
+
log_pass "$DISPLAY_NAME: enabled (CLI deploy)"
|
|
971
|
+
return 0
|
|
972
|
+
fi
|
|
973
|
+
log_warn "$DISPLAY_NAME: CLI deploy succeeded but SOAP verification failed"
|
|
974
|
+
else
|
|
975
|
+
local DEPLOY_ERR
|
|
976
|
+
DEPLOY_ERR=$(safe_jq "$DEPLOY_RESULT" '.result.details.componentFailures[0].problem // .message // "unknown"' "unknown")
|
|
977
|
+
echo " CLI deploy error: $DEPLOY_ERR"
|
|
978
|
+
fi
|
|
979
|
+
else
|
|
980
|
+
echo " CLI retrieve returned no settings file for $CLI_NAME"
|
|
981
|
+
fi
|
|
982
|
+
|
|
983
|
+
# --- APPROACH 3: REST Composite API (for settings that support it) ---
|
|
984
|
+
log_try "$DISPLAY_NAME: trying REST API PATCH..."
|
|
985
|
+
show_progress "REST API PATCH for $DISPLAY_NAME"
|
|
986
|
+
local REST_RESULT
|
|
987
|
+
REST_RESULT=$(curl -s --max-time $CURL_TIMEOUT -X PATCH "${INSTANCE_URL}/services/data/v${API_VERSION}/tooling/sobjects/${META_TYPE}/${FULL_NAME}" \
|
|
988
|
+
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
|
|
989
|
+
-H "Content-Type: application/json" \
|
|
990
|
+
-d "{\"${FIELD}\": true}" 2>/dev/null || echo "")
|
|
991
|
+
clear_progress
|
|
992
|
+
|
|
993
|
+
# REST may return empty on success (204) or an error body
|
|
994
|
+
if [[ -z "$REST_RESULT" ]] || echo "$REST_RESULT" | grep -qi "\"success\":true\|\"id\""; then
|
|
995
|
+
sleep $SLEEP_MEDIUM
|
|
996
|
+
CURRENT=$(soap_extract_bool "$(soap_read "$META_TYPE" "$FULL_NAME")" "$FIELD")
|
|
997
|
+
if [[ "$CURRENT" == "true" ]]; then
|
|
998
|
+
log_pass "$DISPLAY_NAME: enabled (REST API)"
|
|
999
|
+
return 0
|
|
1000
|
+
fi
|
|
1001
|
+
fi
|
|
1002
|
+
|
|
1003
|
+
# --- ALL APPROACHES EXHAUSTED ---
|
|
1004
|
+
# Final verification attempt after waiting
|
|
1005
|
+
sleep $SLEEP_LONG
|
|
1006
|
+
CURRENT=$(soap_extract_bool "$(soap_read "$META_TYPE" "$FULL_NAME")" "$FIELD")
|
|
1007
|
+
if [[ "$CURRENT" == "true" ]]; then
|
|
1008
|
+
log_pass "$DISPLAY_NAME: enabled (delayed propagation)"
|
|
1009
|
+
return 0
|
|
1010
|
+
fi
|
|
1011
|
+
|
|
1012
|
+
log_fail "$DISPLAY_NAME: COULD NOT ENABLE (all 3 approaches failed)"
|
|
1013
|
+
return 1
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
# ============================================================
|
|
1017
|
+
# Phase 2: Prerequisites (with verification gate)
|
|
1018
|
+
# ============================================================
|
|
1019
|
+
section "Phase 2: Prerequisites"
|
|
1020
|
+
|
|
1021
|
+
PREREQ_BLOCKERS=0
|
|
1022
|
+
|
|
1023
|
+
# --- 2a: Einstein Generative AI (CRITICAL — everything depends on this) ---
|
|
1024
|
+
GENAI_RESULT=0
|
|
1025
|
+
enable_setting "EinsteinGptSettings" "EinsteinGpt" "enableEinsteinGptPlatform" "Einstein Generative AI" || GENAI_RESULT=$?
|
|
1026
|
+
|
|
1027
|
+
if [[ "$GENAI_RESULT" -eq 2 ]]; then
|
|
1028
|
+
# Metadata type not available (INVALID_TYPE) - org lacks required license/edition
|
|
1029
|
+
echo ""
|
|
1030
|
+
echo " BLOCKER: Einstein Generative AI metadata type not available in this org."
|
|
1031
|
+
echo " This is the foundation for ALL Agentforce features."
|
|
1032
|
+
echo ""
|
|
1033
|
+
echo " Root cause: EinsteinGptSettings metadata type returned INVALID_TYPE"
|
|
1034
|
+
echo " This means the org is missing:"
|
|
1035
|
+
echo " - Enterprise Edition (or higher) with Einstein features"
|
|
1036
|
+
echo " - Einstein Generative AI licenses"
|
|
1037
|
+
echo " - Platform-level Einstein feature enablement"
|
|
1038
|
+
echo ""
|
|
1039
|
+
echo " Resolution:"
|
|
1040
|
+
echo " 1. Verify org edition: Setup → Company Information"
|
|
1041
|
+
echo " Required: Enterprise Edition or higher"
|
|
1042
|
+
echo " 2. Check licenses: Setup → Company Information → User Licenses"
|
|
1043
|
+
echo " Look for: 'Einstein', 'Sales Cloud Einstein', 'Service Cloud Einstein'"
|
|
1044
|
+
echo " 3. If licenses are missing:"
|
|
1045
|
+
echo " → Contact your Salesforce account team to provision Einstein licenses"
|
|
1046
|
+
echo " → Trial orgs: Request Einstein trial extension"
|
|
1047
|
+
echo " 4. Re-run this script: ./setup-all.sh $ORG_ALIAS"
|
|
1048
|
+
echo ""
|
|
1049
|
+
PREREQ_BLOCKERS=$((PREREQ_BLOCKERS + 1))
|
|
1050
|
+
elif [[ "$GENAI_RESULT" -ne 0 ]]; then
|
|
1051
|
+
# Other failure (return code 1) - could be ToS, permissions, or other config issue
|
|
1052
|
+
echo ""
|
|
1053
|
+
echo " BLOCKER: Einstein Generative AI cannot be enabled."
|
|
1054
|
+
echo " This is the foundation for ALL Agentforce features."
|
|
1055
|
+
echo ""
|
|
1056
|
+
echo " Common causes:"
|
|
1057
|
+
echo " - Org edition doesn't include Einstein GenAI features"
|
|
1058
|
+
echo " - Einstein Terms of Service not accepted (Setup → Einstein Setup)"
|
|
1059
|
+
echo " - Org-level feature toggle disabled by Salesforce"
|
|
1060
|
+
echo " - Missing required licenses for Einstein features"
|
|
1061
|
+
echo ""
|
|
1062
|
+
echo " Manual resolution:"
|
|
1063
|
+
echo " 1. Setup → Einstein Setup → Review and accept Terms of Service"
|
|
1064
|
+
echo " 2. Setup → Einstein Generative AI → Enable"
|
|
1065
|
+
echo " 3. Check Setup → Company Information → User Licenses for Einstein/Agentforce licenses"
|
|
1066
|
+
echo ""
|
|
1067
|
+
PREREQ_BLOCKERS=$((PREREQ_BLOCKERS + 1))
|
|
1068
|
+
fi
|
|
1069
|
+
|
|
1070
|
+
# --- 2b: Agentforce Agent (CRITICAL — depends on GenAI) ---
|
|
1071
|
+
COPILOT_RESULT=0
|
|
1072
|
+
enable_setting "EinsteinCopilotSettings" "EinsteinCopilot" "enableEinsteinGptCopilot" "Agentforce Agent" || COPILOT_RESULT=$?
|
|
1073
|
+
|
|
1074
|
+
if [[ "$COPILOT_RESULT" -eq 2 ]]; then
|
|
1075
|
+
# Metadata type not available (INVALID_TYPE) - org lacks required license/edition
|
|
1076
|
+
echo ""
|
|
1077
|
+
echo " BLOCKER: Agentforce Agent metadata type not available in this org."
|
|
1078
|
+
echo " This indicates the org does NOT have the required Salesforce edition or licenses."
|
|
1079
|
+
echo ""
|
|
1080
|
+
echo " Root cause: EinsteinCopilotSettings metadata type returned INVALID_TYPE"
|
|
1081
|
+
echo " This means the org is missing:"
|
|
1082
|
+
echo " - Enterprise Edition (or higher) with Einstein/Agentforce features"
|
|
1083
|
+
echo " - Einstein Agent licenses"
|
|
1084
|
+
echo " - Agentforce for Sales add-on licenses"
|
|
1085
|
+
echo ""
|
|
1086
|
+
echo " Resolution:"
|
|
1087
|
+
echo " 1. Verify org edition: Setup → Company Information"
|
|
1088
|
+
echo " Required: Enterprise Edition or higher"
|
|
1089
|
+
echo " 2. Check licenses: Setup → Company Information → User Licenses"
|
|
1090
|
+
echo " Look for: 'Einstein Agent', 'Agentforce for Sales', 'Sales Cloud Einstein'"
|
|
1091
|
+
echo " 3. If licenses are missing:"
|
|
1092
|
+
echo " → Contact your Salesforce account team to provision licenses"
|
|
1093
|
+
echo " → Trial orgs: Request Einstein/Agentforce trial extension"
|
|
1094
|
+
echo " 4. Re-run this script: ./setup-all.sh $ORG_ALIAS"
|
|
1095
|
+
echo ""
|
|
1096
|
+
PREREQ_BLOCKERS=$((PREREQ_BLOCKERS + 1))
|
|
1097
|
+
elif [[ "$COPILOT_RESULT" -ne 0 ]]; then
|
|
1098
|
+
# Other failure (return code 1) - could be ToS, permissions, or other config issue
|
|
1099
|
+
echo ""
|
|
1100
|
+
echo " BLOCKER: Agentforce Agent cannot be enabled."
|
|
1101
|
+
echo " Requires Einstein Generative AI (step above) to be enabled first."
|
|
1102
|
+
echo ""
|
|
1103
|
+
echo " Common causes:"
|
|
1104
|
+
echo " - Missing Einstein Agent licenses"
|
|
1105
|
+
echo " - Org edition doesn't include Einstein/Agentforce features"
|
|
1106
|
+
echo " - Einstein Terms of Service not accepted"
|
|
1107
|
+
echo ""
|
|
1108
|
+
# Check if Einstein Agent licenses exist
|
|
1109
|
+
EA_LICENSE=$(get_license_count "Einstein Agent")
|
|
1110
|
+
if [[ "$EA_LICENSE" == "ERROR" ]]; then
|
|
1111
|
+
echo " License Check: ⚠️ Could not verify license status (API error)"
|
|
1112
|
+
echo " → Check org connectivity and try again"
|
|
1113
|
+
echo ""
|
|
1114
|
+
elif [[ "$EA_LICENSE" -eq 0 ]]; then
|
|
1115
|
+
echo " License Check: ❌ No Einstein Agent licenses found"
|
|
1116
|
+
echo " → Contact your Salesforce account team to provision Einstein Agent licenses"
|
|
1117
|
+
echo " → View licenses: Setup → Company Information → User Licenses"
|
|
1118
|
+
echo ""
|
|
1119
|
+
else
|
|
1120
|
+
echo " License Check: ✓ Einstein Agent licenses available ($EA_LICENSE)"
|
|
1121
|
+
echo ""
|
|
1122
|
+
echo " Most likely cause: Einstein Terms of Service NOT accepted"
|
|
1123
|
+
echo ""
|
|
1124
|
+
echo " Required action:"
|
|
1125
|
+
echo " 1. Navigate to: ${INSTANCE_URL}/lightning/setup/EinsteinGPTSetup/home"
|
|
1126
|
+
echo " (Note: Assumes Lightning Experience. For Classic, use Setup menu.)"
|
|
1127
|
+
echo " 2. Review and accept the Einstein Terms of Service"
|
|
1128
|
+
echo " 3. Then enable Agentforce Agent at: ${INSTANCE_URL}/lightning/setup/EinsteinCopilot/home"
|
|
1129
|
+
echo " 4. Re-run this script: ./setup-all.sh $ORG_ALIAS"
|
|
1130
|
+
echo ""
|
|
1131
|
+
fi
|
|
1132
|
+
PREREQ_BLOCKERS=$((PREREQ_BLOCKERS + 1))
|
|
1133
|
+
fi
|
|
1134
|
+
|
|
1135
|
+
# --- 2b2: Agentforce Studio / Agent Platform (CRITICAL — gates Deal Agent) ---
|
|
1136
|
+
# AgentPlatformSettings.enableAgentPlatform (OrgPreference AgentPlatformEnabled)
|
|
1137
|
+
# must be ON before SalesDealAgentSettings can be enabled. Core enforces this:
|
|
1138
|
+
# DealAgentEnabledOrgPreference.validateAgentforcePlatformAndLog() throws if the
|
|
1139
|
+
# Agentforce Platform preference is not enabled, so a missing step here surfaces
|
|
1140
|
+
# later as an opaque Deal Agent activation failure. Ordering: GenAI → Copilot →
|
|
1141
|
+
# AgentPlatform → SalesDealAgent.
|
|
1142
|
+
AGENT_PLATFORM_RESULT=0
|
|
1143
|
+
enable_setting "AgentPlatformSettings" "AgentPlatform" "enableAgentPlatform" "Agentforce Studio (Agent Platform)" || AGENT_PLATFORM_RESULT=$?
|
|
1144
|
+
|
|
1145
|
+
if [[ "$AGENT_PLATFORM_RESULT" -eq 2 ]]; then
|
|
1146
|
+
# Metadata type not available (INVALID_TYPE) - org lacks required license/edition
|
|
1147
|
+
echo ""
|
|
1148
|
+
echo " BLOCKER: Agentforce Studio (Agent Platform) metadata type not available in this org."
|
|
1149
|
+
echo " AgentPlatformSettings returned INVALID_TYPE — the org is missing the"
|
|
1150
|
+
echo " Enterprise Edition (or higher) or Agentforce licenses required for Agent Platform."
|
|
1151
|
+
echo ""
|
|
1152
|
+
echo " Resolution:"
|
|
1153
|
+
echo " 1. Verify org edition: Setup → Company Information (Enterprise Edition or higher)"
|
|
1154
|
+
echo " 2. Check licenses: Setup → Company Information → User Licenses"
|
|
1155
|
+
echo " Look for: 'Agentforce', 'Einstein Agent', 'Agentforce for Sales'"
|
|
1156
|
+
echo " 3. Re-run this script: ./setup-all.sh $ORG_ALIAS"
|
|
1157
|
+
echo ""
|
|
1158
|
+
PREREQ_BLOCKERS=$((PREREQ_BLOCKERS + 1))
|
|
1159
|
+
elif [[ "$AGENT_PLATFORM_RESULT" -ne 0 ]]; then
|
|
1160
|
+
echo ""
|
|
1161
|
+
echo " BLOCKER: Agentforce Studio (Agent Platform) cannot be enabled."
|
|
1162
|
+
echo " This preference (AgentPlatformEnabled) gates the Sales Deal Agent —"
|
|
1163
|
+
echo " Deal Agent activation fails downstream if it is not enabled first."
|
|
1164
|
+
echo ""
|
|
1165
|
+
echo " Common causes:"
|
|
1166
|
+
echo " - Einstein Generative AI / Agentforce Agent not enabled (steps above)"
|
|
1167
|
+
echo " - Missing Agentforce licenses"
|
|
1168
|
+
echo " - Einstein Terms of Service not accepted"
|
|
1169
|
+
echo ""
|
|
1170
|
+
echo " Manual resolution:"
|
|
1171
|
+
echo " 1. Enable Einstein GenAI and Agentforce Agent (steps above)"
|
|
1172
|
+
echo " 2. Navigate to: ${INSTANCE_URL}/lightning/setup/EinsteinAgentBuilder/home"
|
|
1173
|
+
echo " (Agentforce Studio) and confirm the platform is enabled"
|
|
1174
|
+
echo " 3. Re-run this script: ./setup-all.sh $ORG_ALIAS"
|
|
1175
|
+
echo ""
|
|
1176
|
+
PREREQ_BLOCKERS=$((PREREQ_BLOCKERS + 1))
|
|
1177
|
+
fi
|
|
1178
|
+
|
|
1179
|
+
# --- 2c: Enhanced Notes (CRITICAL — PM data source) ---
|
|
1180
|
+
if ! enable_setting "EnhancedNotesSettings" "EnhancedNotes" "enableEnhancedNotes" "Enhanced Notes"; then
|
|
1181
|
+
echo ""
|
|
1182
|
+
echo " BLOCKER: Enhanced Notes cannot be enabled."
|
|
1183
|
+
echo " Pipeline Management uses ContentNote as a primary data source."
|
|
1184
|
+
echo ""
|
|
1185
|
+
PREREQ_BLOCKERS=$((PREREQ_BLOCKERS + 1))
|
|
1186
|
+
fi
|
|
1187
|
+
|
|
1188
|
+
# --- 2d: Opportunity Team (CRITICAL — flow uses OpportunityTeamMember) ---
|
|
1189
|
+
if ! enable_setting "OpportunitySettings" "Opportunity" "enableOpportunityTeam" "Opportunity Team"; then
|
|
1190
|
+
echo ""
|
|
1191
|
+
echo " BLOCKER: Opportunity Team cannot be enabled."
|
|
1192
|
+
echo " Pipeline Management flow requires OpportunityTeamMember object access."
|
|
1193
|
+
echo ""
|
|
1194
|
+
PREREQ_BLOCKERS=$((PREREQ_BLOCKERS + 1))
|
|
1195
|
+
fi
|
|
1196
|
+
|
|
1197
|
+
# --- 2e: Pipeline Inspection (required for UI review of suggestions) ---
|
|
1198
|
+
enable_setting "OpportunitySettings" "Opportunity" "enablePipelineInspection" "Pipeline Inspection" || \
|
|
1199
|
+
log_warn "Pipeline Inspection not enabled — suggestions generate but users have no UI to review/accept them"
|
|
1200
|
+
|
|
1201
|
+
# --- 2f: Enhanced Email (optional) ---
|
|
1202
|
+
enable_setting "EmailAdministrationSettings" "EmailAdministration" "enableEnhancedEmailEnabled" "Enhanced Email" || \
|
|
1203
|
+
log_warn "Enhanced Email not enabled (email body indexing unavailable — agent uses notes instead)"
|
|
1204
|
+
|
|
1205
|
+
# --- VERIFICATION GATE: All critical prerequisites must pass ---
|
|
1206
|
+
echo ""
|
|
1207
|
+
echo " --- Prerequisite Verification Gate ---"
|
|
1208
|
+
|
|
1209
|
+
GATE_PASS=true
|
|
1210
|
+
|
|
1211
|
+
# Classify each gate setting so a real "disabled" is distinguished from an
|
|
1212
|
+
# auth/SOAP fault or a network/service error (W-23215519). gate_check logs the
|
|
1213
|
+
# right diagnostic and fails the gate for any non-"true" outcome.
|
|
1214
|
+
# Args: $1 = display label, $2 = classification string from soap_classify
|
|
1215
|
+
gate_check() {
|
|
1216
|
+
local label="$1" status="$2"
|
|
1217
|
+
case "$status" in
|
|
1218
|
+
true)
|
|
1219
|
+
return 0
|
|
1220
|
+
;;
|
|
1221
|
+
false)
|
|
1222
|
+
log_fail "GATE: $label NOT confirmed (setting is disabled)"
|
|
1223
|
+
;;
|
|
1224
|
+
AUTH_ERROR:*)
|
|
1225
|
+
log_fail "GATE: $label cannot be verified — auth/SOAP fault (${status#AUTH_ERROR:})"
|
|
1226
|
+
echo " Re-authenticate: sf org login web --alias $ORG_ALIAS"
|
|
1227
|
+
;;
|
|
1228
|
+
TYPE_UNAVAILABLE:*)
|
|
1229
|
+
log_fail "GATE: $label cannot be verified — metadata type unavailable (${status#TYPE_UNAVAILABLE:})"
|
|
1230
|
+
;;
|
|
1231
|
+
NETWORK_ERROR)
|
|
1232
|
+
log_fail "GATE: $label cannot be verified — network/service error (check connectivity)"
|
|
1233
|
+
;;
|
|
1234
|
+
*)
|
|
1235
|
+
log_fail "GATE: $label NOT confirmed (unexpected status: $status)"
|
|
1236
|
+
;;
|
|
1237
|
+
esac
|
|
1238
|
+
GATE_PASS=false
|
|
1239
|
+
return 1
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
GENAI_STATUS=$(soap_classify "$(soap_read "EinsteinGptSettings" "EinsteinGpt")" "enableEinsteinGptPlatform")
|
|
1243
|
+
COPILOT_STATUS=$(soap_classify "$(soap_read "EinsteinCopilotSettings" "EinsteinCopilot")" "enableEinsteinGptCopilot")
|
|
1244
|
+
AGENT_PLATFORM_STATUS=$(soap_classify "$(soap_read "AgentPlatformSettings" "AgentPlatform")" "enableAgentPlatform")
|
|
1245
|
+
NOTES_STATUS=$(soap_classify "$(soap_read "EnhancedNotesSettings" "EnhancedNotes")" "enableEnhancedNotes")
|
|
1246
|
+
OPP_TEAM_STATUS=$(soap_classify "$(soap_read "OpportunitySettings" "Opportunity")" "enableOpportunityTeam")
|
|
1247
|
+
|
|
1248
|
+
# Keep boolean mirrors for the existing failure-summary conditionals below.
|
|
1249
|
+
VERIFY_GENAI=$([[ "$GENAI_STATUS" == "true" ]] && echo "true" || echo "false")
|
|
1250
|
+
VERIFY_COPILOT=$([[ "$COPILOT_STATUS" == "true" ]] && echo "true" || echo "false")
|
|
1251
|
+
VERIFY_AGENT_PLATFORM=$([[ "$AGENT_PLATFORM_STATUS" == "true" ]] && echo "true" || echo "false")
|
|
1252
|
+
VERIFY_NOTES=$([[ "$NOTES_STATUS" == "true" ]] && echo "true" || echo "false")
|
|
1253
|
+
VERIFY_OPP_TEAM=$([[ "$OPP_TEAM_STATUS" == "true" ]] && echo "true" || echo "false")
|
|
1254
|
+
|
|
1255
|
+
gate_check "Einstein Generative AI" "$GENAI_STATUS" || true
|
|
1256
|
+
gate_check "Agentforce Agent" "$COPILOT_STATUS" || true
|
|
1257
|
+
gate_check "Agentforce Studio (Agent Platform)" "$AGENT_PLATFORM_STATUS" || true
|
|
1258
|
+
gate_check "Enhanced Notes" "$NOTES_STATUS" || true
|
|
1259
|
+
gate_check "Opportunity Team" "$OPP_TEAM_STATUS" || true
|
|
1260
|
+
|
|
1261
|
+
if [[ "$GATE_PASS" != "true" ]]; then
|
|
1262
|
+
echo ""
|
|
1263
|
+
log_fail "PREREQUISITE GATE FAILED — cannot proceed to Pipeline Management enablement."
|
|
1264
|
+
echo ""
|
|
1265
|
+
echo " The following critical settings must be enabled before PM can be activated:"
|
|
1266
|
+
[[ "$VERIFY_GENAI" != "true" ]] && echo " ❌ EinsteinGptSettings.enableEinsteinGptPlatform"
|
|
1267
|
+
[[ "$VERIFY_COPILOT" != "true" ]] && echo " ❌ EinsteinCopilotSettings.enableEinsteinGptCopilot"
|
|
1268
|
+
[[ "$VERIFY_AGENT_PLATFORM" != "true" ]] && echo " ❌ AgentPlatformSettings.enableAgentPlatform"
|
|
1269
|
+
[[ "$VERIFY_NOTES" != "true" ]] && echo " ❌ EnhancedNotesSettings.enableEnhancedNotes"
|
|
1270
|
+
[[ "$VERIFY_OPP_TEAM" != "true" ]] && echo " ❌ OpportunitySettings.enableOpportunityTeam"
|
|
1271
|
+
echo ""
|
|
1272
|
+
echo " Manual resolution:"
|
|
1273
|
+
echo " 1. Setup → Einstein → Einstein Generative AI → Enable"
|
|
1274
|
+
echo " 2. Setup → Einstein → Copilot → Enable"
|
|
1275
|
+
echo " 3. Setup → Agentforce Studio → Enable (Agent Platform)"
|
|
1276
|
+
echo " 4. Setup → Notes → Enable Enhanced Notes"
|
|
1277
|
+
echo " 5. Setup → Opportunity Settings → Enable Opportunity Team"
|
|
1278
|
+
echo " 6. Re-run: bash setup-all.sh $ORG_ALIAS"
|
|
1279
|
+
exit 1
|
|
1280
|
+
fi
|
|
1281
|
+
|
|
1282
|
+
log_pass "GATE: All critical prerequisites verified ✓"
|
|
1283
|
+
|
|
1284
|
+
# ============================================================
|
|
1285
|
+
# Phase 2.5: License and Metadata Type Validation
|
|
1286
|
+
# ============================================================
|
|
1287
|
+
echo ""
|
|
1288
|
+
echo " --- Pipeline Management Compatibility Gate ---"
|
|
1289
|
+
|
|
1290
|
+
# Expected license names: "Agentforce for Sales", "Sales Cloud Einstein", "Agentforce 1"
|
|
1291
|
+
# If Salesforce introduces new variants, this query may need updating
|
|
1292
|
+
SALES_LICENSE_OUTPUT=$(sf data query --query "SELECT COUNT(Id) FROM UserLicense WHERE (Name LIKE '%Agentforce for Sales%' OR Name LIKE '%Sales Cloud Einstein%' OR Name LIKE '%Agentforce 1%') AND TotalLicenses > 0" --target-org "$ORG_ALIAS" --json 2>/dev/null)
|
|
1293
|
+
SALES_LICENSE_COUNT=$(safe_jq_int "$SALES_LICENSE_OUTPUT" '.result.records[0].expr0 // 0')
|
|
1294
|
+
|
|
1295
|
+
EINSTEIN_AGENT_COUNT=$(get_license_count "Einstein Agent")
|
|
1296
|
+
# Handle API errors gracefully
|
|
1297
|
+
if [[ "$EINSTEIN_AGENT_COUNT" == "ERROR" ]]; then
|
|
1298
|
+
EINSTEIN_AGENT_COUNT="unknown"
|
|
1299
|
+
fi
|
|
1300
|
+
|
|
1301
|
+
# Check if SalesDealAgentSettings metadata type is available
|
|
1302
|
+
METADATA_TYPE_CHECK=$(soap_read "SalesDealAgentSettings" "SalesDealAgent" 2>/dev/null || echo "")
|
|
1303
|
+
|
|
1304
|
+
# Backstop only — the Step 0 gate (Phase 0.5) is the primary check and normally
|
|
1305
|
+
# stops an unlicensed org before any question. This catches a post-enablement
|
|
1306
|
+
# regression (e.g. the type going away mid-run). Shares print_license_blocker().
|
|
1307
|
+
if echo "$METADATA_TYPE_CHECK" | grep -q "INVALID_TYPE"; then
|
|
1308
|
+
log_fail "GATE: SalesDealAgentSettings metadata type NOT available"
|
|
1309
|
+
print_license_blocker "$EINSTEIN_AGENT_COUNT" "$SALES_LICENSE_COUNT"
|
|
1310
|
+
exit 1
|
|
1311
|
+
fi
|
|
1312
|
+
|
|
1313
|
+
if [[ "$SALES_LICENSE_COUNT" -eq 0 ]]; then
|
|
1314
|
+
log_warn "GATE: No Agentforce for Sales licenses found (but metadata type exists)"
|
|
1315
|
+
echo ""
|
|
1316
|
+
echo " Warning: The org has the metadata type but no explicit Sales licenses."
|
|
1317
|
+
echo " Pipeline Management may work via base Einstein Agent licenses ($EINSTEIN_AGENT_COUNT)."
|
|
1318
|
+
echo " Proceeding with enablement attempt..."
|
|
1319
|
+
echo ""
|
|
1320
|
+
else
|
|
1321
|
+
log_pass "GATE: Agentforce for Sales licenses found ($SALES_LICENSE_COUNT) ✓"
|
|
1322
|
+
fi
|
|
1323
|
+
|
|
1324
|
+
# ============================================================
|
|
1325
|
+
# Phase 3: Enable Pipeline Management (with multi-approach + verification)
|
|
1326
|
+
# ============================================================
|
|
1327
|
+
section "Phase 3: Pipeline Management"
|
|
1328
|
+
|
|
1329
|
+
PM_ENABLED=false
|
|
1330
|
+
|
|
1331
|
+
# Check current state
|
|
1332
|
+
PM_CURRENT=$(soap_extract_bool "$(soap_read "SalesDealAgentSettings" "SalesDealAgent")" "enableDealAgent")
|
|
1333
|
+
if [[ "$PM_CURRENT" == "true" ]]; then
|
|
1334
|
+
log_pass "Pipeline Management: already enabled"
|
|
1335
|
+
PM_ENABLED=true
|
|
1336
|
+
fi
|
|
1337
|
+
|
|
1338
|
+
if [[ "$PM_ENABLED" != "true" ]]; then
|
|
1339
|
+
# --- APPROACH 1: SOAP updateMetadata ---
|
|
1340
|
+
log_try "Enabling via SOAP Metadata API v${API_VERSION}..."
|
|
1341
|
+
RESULT=$(soap_update "<met:metadata xsi:type='met:SalesDealAgentSettings' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'><met:fullName>SalesDealAgent</met:fullName><met:enableDealAgent>true</met:enableDealAgent><met:enableDealAgentAutoApproveAllTasks>false</met:enableDealAgentAutoApproveAllTasks></met:metadata>")
|
|
1342
|
+
|
|
1343
|
+
if echo "$RESULT" | grep -q "<success>true</success>"; then
|
|
1344
|
+
sleep $SLEEP_MEDIUM
|
|
1345
|
+
PM_CHECK=$(soap_extract_bool "$(soap_read "SalesDealAgentSettings" "SalesDealAgent")" "enableDealAgent")
|
|
1346
|
+
if [[ "$PM_CHECK" == "true" ]]; then
|
|
1347
|
+
log_pass "Pipeline Management: enabled (SOAP)"
|
|
1348
|
+
PM_ENABLED=true
|
|
1349
|
+
fi
|
|
1350
|
+
fi
|
|
1351
|
+
|
|
1352
|
+
if [[ "$PM_ENABLED" != "true" ]]; then
|
|
1353
|
+
SOAP_ERR=$(echo "$RESULT" | grep -o '<message>[^<]*</message>' | sed 's/<[^>]*>//g' | head -1 || echo "")
|
|
1354
|
+
[[ -n "$SOAP_ERR" ]] && echo " SOAP error: $(redact_token "$SOAP_ERR")"
|
|
1355
|
+
fi
|
|
1356
|
+
fi
|
|
1357
|
+
|
|
1358
|
+
if [[ "$PM_ENABLED" != "true" ]]; then
|
|
1359
|
+
# --- APPROACH 2: CLI Metadata deploy (SalesDealAgent settings XML) ---
|
|
1360
|
+
log_try "Enabling via CLI Metadata deploy..."
|
|
1361
|
+
ensure_sfdx_project
|
|
1362
|
+
mkdir -p force-app/main/default/settings
|
|
1363
|
+
|
|
1364
|
+
# Create settings XML directly (CLI retrieve fails for this type)
|
|
1365
|
+
cat > force-app/main/default/settings/SalesDealAgent.settings-meta.xml << 'XMLEOF'
|
|
1366
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
1367
|
+
<SalesDealAgentSettings xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
1368
|
+
<enableDealAgent>true</enableDealAgent>
|
|
1369
|
+
<enableDealAgentAutoApproveAllTasks>false</enableDealAgentAutoApproveAllTasks>
|
|
1370
|
+
</SalesDealAgentSettings>
|
|
1371
|
+
XMLEOF
|
|
1372
|
+
|
|
1373
|
+
DEPLOY_RESULT=$(sf project deploy start --source-dir force-app/main/default/settings/SalesDealAgent.settings-meta.xml --target-org "$ORG_ALIAS" --json 2>/dev/null || echo '{"status":1}')
|
|
1374
|
+
if echo "$DEPLOY_RESULT" | jq -e '.status == 0' >/dev/null 2>&1; then
|
|
1375
|
+
sleep $SLEEP_LONG
|
|
1376
|
+
PM_CHECK=$(soap_extract_bool "$(soap_read "SalesDealAgentSettings" "SalesDealAgent")" "enableDealAgent")
|
|
1377
|
+
if [[ "$PM_CHECK" == "true" ]]; then
|
|
1378
|
+
log_pass "Pipeline Management: enabled (CLI deploy)"
|
|
1379
|
+
PM_ENABLED=true
|
|
1380
|
+
fi
|
|
1381
|
+
else
|
|
1382
|
+
DEPLOY_ERR=$(safe_jq "$DEPLOY_RESULT" '.result.details.componentFailures[0].problem // .message // "unknown"' "unknown")
|
|
1383
|
+
echo " CLI deploy error: $DEPLOY_ERR"
|
|
1384
|
+
fi
|
|
1385
|
+
fi
|
|
1386
|
+
|
|
1387
|
+
if [[ "$PM_ENABLED" != "true" ]]; then
|
|
1388
|
+
# --- APPROACH 3: Recognize genuine mid-provisioning WITHOUT asserting enablement ---
|
|
1389
|
+
# #9: PSG existence is NOT a setup signal — SalesManagementUserPsg /
|
|
1390
|
+
# SalesManagementAgentUserPsg ship with the Agentforce-for-Sales license and exist even
|
|
1391
|
+
# in a never-configured org, so "PSGs exist" says nothing about whether PM was set up.
|
|
1392
|
+
# The authoritative signal is SalesDealAgentSettings.enableDealAgent via SOAP readMetadata
|
|
1393
|
+
# (checked above). Here we re-read it authoritatively; only if it reads true do we mark
|
|
1394
|
+
# enabled. If it's still false, we check the REAL "setup has run" signals — an agent user
|
|
1395
|
+
# holding SalesManagementAgentUserPsg, a cloned scheduled flow, or a PM BotDefinition. If
|
|
1396
|
+
# any is present, a bundle-publish/template-provisioned org may be mid-flight, so we mark
|
|
1397
|
+
# "unconfirmed" and continue, letting the downstream verification gate confirm. Otherwise
|
|
1398
|
+
# we fall through to Approach 4.
|
|
1399
|
+
log_try "Re-reading enableDealAgent authoritatively (SOAP) and checking real setup signals..."
|
|
1400
|
+
PM_RECHECK=$(soap_extract_bool "$(soap_read "SalesDealAgentSettings" "SalesDealAgent")" "enableDealAgent")
|
|
1401
|
+
|
|
1402
|
+
if [[ "$PM_RECHECK" == "true" ]]; then
|
|
1403
|
+
log_pass "Pipeline Management: enabled (confirmed via SOAP enableDealAgent)"
|
|
1404
|
+
PM_ENABLED=true
|
|
1405
|
+
else
|
|
1406
|
+
AGENT_PSG_HELD=$(pm_agent_user_holds_psg "$ORG_ALIAS")
|
|
1407
|
+
PM_FLOW_EXISTS=$(pm_flow_clone_exists "$ORG_ALIAS")
|
|
1408
|
+
PM_BOT_EXISTS=$(pm_bot_count "$ORG_ALIAS")
|
|
1409
|
+
if [[ "$AGENT_PSG_HELD" -ge 1 || "$PM_FLOW_EXISTS" -ge 1 || "$PM_BOT_EXISTS" -ge 1 ]]; then
|
|
1410
|
+
log_warn "enableDealAgent not confirmed true, but real setup signals exist (agentPSG=$AGENT_PSG_HELD flow=$PM_FLOW_EXISTS bot=$PM_BOT_EXISTS) — provisioning likely in progress."
|
|
1411
|
+
echo " Continuing with setup; enablement is UNCONFIRMED and will be re-verified in Phase 8."
|
|
1412
|
+
PM_ENABLED="unconfirmed"
|
|
1413
|
+
fi
|
|
1414
|
+
fi
|
|
1415
|
+
fi
|
|
1416
|
+
|
|
1417
|
+
if [[ "$PM_ENABLED" != "true" && "$PM_ENABLED" != "unconfirmed" ]]; then
|
|
1418
|
+
# --- APPROACH 4: OrgPreference via Tooling API ---
|
|
1419
|
+
log_try "Enabling via OrgPreference Tooling API..."
|
|
1420
|
+
show_progress "Tooling API OrgPreference update"
|
|
1421
|
+
PREF_RESULT=$(curl -s --max-time $CURL_TIMEOUT -X PATCH "${INSTANCE_URL}/services/data/v${API_VERSION}/tooling/sobjects/OrganizationSettingsDetail/DealAgentEnabled" \
|
|
1422
|
+
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
|
|
1423
|
+
-H "Content-Type: application/json" \
|
|
1424
|
+
-d '{"value": true}' 2>/dev/null || echo "")
|
|
1425
|
+
clear_progress
|
|
1426
|
+
|
|
1427
|
+
sleep $SLEEP_LONG
|
|
1428
|
+
PM_CHECK=$(soap_extract_bool "$(soap_read "SalesDealAgentSettings" "SalesDealAgent")" "enableDealAgent")
|
|
1429
|
+
if [[ "$PM_CHECK" == "true" ]]; then
|
|
1430
|
+
log_pass "Pipeline Management: enabled (Tooling API OrgPreference)"
|
|
1431
|
+
PM_ENABLED=true
|
|
1432
|
+
else
|
|
1433
|
+
# Do NOT treat PSG presence as a provisioning signal (the PM PSGs ship with the
|
|
1434
|
+
# license). Re-check the same real signals as Approach 3: an agent user holding
|
|
1435
|
+
# SalesManagementAgentUserPsg, a cloned scheduled flow, or a PM BotDefinition.
|
|
1436
|
+
AGENT_PSG_HELD=$(pm_agent_user_holds_psg "$ORG_ALIAS")
|
|
1437
|
+
PM_FLOW_EXISTS=$(pm_flow_clone_exists "$ORG_ALIAS")
|
|
1438
|
+
PM_BOT_EXISTS=$(pm_bot_count "$ORG_ALIAS")
|
|
1439
|
+
if [[ "$AGENT_PSG_HELD" -ge 1 || "$PM_FLOW_EXISTS" -ge 1 || "$PM_BOT_EXISTS" -ge 1 ]]; then
|
|
1440
|
+
log_warn "Tooling API approach uncertain, but real setup signals appeared (agentPSG=$AGENT_PSG_HELD flow=$PM_FLOW_EXISTS bot=$PM_BOT_EXISTS) — PM provisioning in progress"
|
|
1441
|
+
PM_ENABLED="unconfirmed"
|
|
1442
|
+
fi
|
|
1443
|
+
fi
|
|
1444
|
+
fi
|
|
1445
|
+
|
|
1446
|
+
# --- PM VERIFICATION GATE ---
|
|
1447
|
+
if [[ "$PM_ENABLED" != "true" && "$PM_ENABLED" != "unconfirmed" ]]; then
|
|
1448
|
+
echo ""
|
|
1449
|
+
log_fail "PIPELINE MANAGEMENT GATE FAILED — all 4 approaches exhausted."
|
|
1450
|
+
echo ""
|
|
1451
|
+
echo " Attempted:"
|
|
1452
|
+
echo " 1. SOAP Metadata API v${API_VERSION} (updateMetadata SalesDealAgentSettings)"
|
|
1453
|
+
echo " 2. CLI Metadata deploy (settings XML)"
|
|
1454
|
+
echo " 3. Authoritative SOAP re-read + provisioning-signal detection (agent-user / flow / bot presence)"
|
|
1455
|
+
echo " 4. Tooling API OrgPreference (DealAgentEnabled)"
|
|
1456
|
+
echo ""
|
|
1457
|
+
echo " This org may require manual enablement:"
|
|
1458
|
+
echo " Setup → Sales → Pipeline Management → Enable toggle"
|
|
1459
|
+
echo " URL: ${INSTANCE_URL}/lightning/setup/PipelineManagement/home"
|
|
1460
|
+
echo ""
|
|
1461
|
+
echo " After enabling, re-run: bash setup-all.sh $ORG_ALIAS"
|
|
1462
|
+
exit 1
|
|
1463
|
+
fi
|
|
1464
|
+
|
|
1465
|
+
# ============================================================
|
|
1466
|
+
# Phase 3.5: Enable Autonomous Field Updates
|
|
1467
|
+
# ============================================================
|
|
1468
|
+
section "Phase 3.5: Autonomous Field Updates"
|
|
1469
|
+
|
|
1470
|
+
# After Pipeline Management is enabled, set enableDealAgentAutoApproveAllTasks=true
|
|
1471
|
+
# to allow the agent to update opportunity fields without user approval.
|
|
1472
|
+
#
|
|
1473
|
+
# CONSENT GATE (#2): autonomous updates are opt-in only. Without --autonomous (or an
|
|
1474
|
+
# interactive "yes" in Phase 0), the agent stays in suggestion-only mode and this phase
|
|
1475
|
+
# makes NO change. This prevents the agent from silently auto-applying field writes.
|
|
1476
|
+
|
|
1477
|
+
AUTO_APPROVE_CURRENT=$(soap_extract_bool "$(soap_read "SalesDealAgentSettings" "SalesDealAgent")" "enableDealAgentAutoApproveAllTasks")
|
|
1478
|
+
|
|
1479
|
+
if [[ "$ENABLE_AUTONOMOUS" != "true" ]]; then
|
|
1480
|
+
if [[ "$AUTO_APPROVE_CURRENT" == "true" ]]; then
|
|
1481
|
+
log_warn "Autonomous updates are currently ON in this org, but --autonomous was not passed."
|
|
1482
|
+
echo " Leaving the existing org setting unchanged (not disabling)."
|
|
1483
|
+
echo " To turn autonomous updates OFF, use the Pipeline Management setup UI."
|
|
1484
|
+
else
|
|
1485
|
+
log_pass "Autonomous updates: OFF (suggestions only) — not enabled without --autonomous"
|
|
1486
|
+
fi
|
|
1487
|
+
elif [[ "$AUTO_APPROVE_CURRENT" == "true" ]]; then
|
|
1488
|
+
log_pass "Autonomous updates: already enabled"
|
|
1489
|
+
else
|
|
1490
|
+
log_info "Enabling autonomous field updates (enableDealAgentAutoApproveAllTasks=true)..."
|
|
1491
|
+
|
|
1492
|
+
# SOAP Metadata API approach
|
|
1493
|
+
RESULT=$(soap_update "<met:metadata xsi:type='met:SalesDealAgentSettings' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'><met:fullName>SalesDealAgent</met:fullName><met:enableDealAgent>true</met:enableDealAgent><met:enableDealAgentAutoApproveAllTasks>true</met:enableDealAgentAutoApproveAllTasks></met:metadata>")
|
|
1494
|
+
|
|
1495
|
+
if echo "$RESULT" | grep -qi "success.*true"; then
|
|
1496
|
+
AUTO_CHECK=$(soap_extract_bool "$(soap_read "SalesDealAgentSettings" "SalesDealAgent")" "enableDealAgentAutoApproveAllTasks")
|
|
1497
|
+
if [[ "$AUTO_CHECK" == "true" ]]; then
|
|
1498
|
+
log_pass "Autonomous updates: enabled (SOAP API)"
|
|
1499
|
+
else
|
|
1500
|
+
log_warn "SOAP update reported success, but verification failed"
|
|
1501
|
+
fi
|
|
1502
|
+
else
|
|
1503
|
+
log_warn "SOAP API update failed, trying CLI metadata deploy..."
|
|
1504
|
+
|
|
1505
|
+
# CLI metadata deploy approach (fallback)
|
|
1506
|
+
TMP_DIR=$(mktemp -d)
|
|
1507
|
+
mkdir -p "$TMP_DIR/settings"
|
|
1508
|
+
|
|
1509
|
+
cat > "$TMP_DIR/settings/SalesDealAgent.settings-meta.xml" <<EOF
|
|
1510
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
1511
|
+
<SalesDealAgentSettings xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
1512
|
+
<enableDealAgent>true</enableDealAgent>
|
|
1513
|
+
<enableDealAgentAutoApproveAllTasks>true</enableDealAgentAutoApproveAllTasks>
|
|
1514
|
+
</SalesDealAgentSettings>
|
|
1515
|
+
EOF
|
|
1516
|
+
|
|
1517
|
+
CLI_RESULT=$(sf project deploy start --metadata-dir "$TMP_DIR/settings" --target-org "$ORG_ALIAS" --json 2>/dev/null || echo '{"status":1}')
|
|
1518
|
+
rm -rf "$TMP_DIR"
|
|
1519
|
+
|
|
1520
|
+
CLI_SUCCESS=$(echo "$CLI_RESULT" | jq -r '.status // 1' 2>/dev/null | head -1 || echo "1")
|
|
1521
|
+
if [[ "$CLI_SUCCESS" == "0" ]]; then
|
|
1522
|
+
AUTO_CHECK=$(soap_extract_bool "$(soap_read "SalesDealAgentSettings" "SalesDealAgent")" "enableDealAgentAutoApproveAllTasks")
|
|
1523
|
+
if [[ "$AUTO_CHECK" == "true" ]]; then
|
|
1524
|
+
log_pass "Autonomous updates: enabled (CLI deploy)"
|
|
1525
|
+
else
|
|
1526
|
+
log_warn "CLI deploy succeeded but verification failed"
|
|
1527
|
+
fi
|
|
1528
|
+
else
|
|
1529
|
+
log_warn "CLI deploy failed. Autonomous updates may require manual enablement:"
|
|
1530
|
+
echo " Setup → Sales → Pipeline Management → Settings"
|
|
1531
|
+
echo " Enable: 'Update Fields Autonomously'"
|
|
1532
|
+
echo " URL: ${INSTANCE_URL}/lightning/setup/PipelineManagement/home"
|
|
1533
|
+
fi
|
|
1534
|
+
fi
|
|
1535
|
+
fi
|
|
1536
|
+
|
|
1537
|
+
# ============================================================
|
|
1538
|
+
# Phase 4: Wait for Provisioning (with polling loop)
|
|
1539
|
+
# ============================================================
|
|
1540
|
+
section "Phase 4: Provisioning (agent user + PSG assignment)"
|
|
1541
|
+
|
|
1542
|
+
MAX_WAIT_SECONDS=90
|
|
1543
|
+
POLL_INTERVAL=5
|
|
1544
|
+
|
|
1545
|
+
# Provisioning is complete when the agent user exists AND holds
|
|
1546
|
+
# SalesManagementAgentUserPsg. The two PM PSGs themselves ship with the
|
|
1547
|
+
# Agentforce-for-Sales license and exist even in a never-configured org, so
|
|
1548
|
+
# "PSG count >= 2" is NOT a completion signal — the agent-user assignment is.
|
|
1549
|
+
check_agent_provisioned() { # sets AGENT_USER (Username) + AGENT_PSG_HELD; returns 0 when complete
|
|
1550
|
+
local rec agent_id
|
|
1551
|
+
rec=$(sf data query --query "SELECT Id, Username FROM User WHERE Username LIKE '%salesmanagementagentuser%'" --target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.records[0] // empty' 2>/dev/null || echo "")
|
|
1552
|
+
AGENT_USER=$(echo "$rec" | jq -r '.Username // empty' 2>/dev/null || echo "")
|
|
1553
|
+
AGENT_PSG_HELD=0
|
|
1554
|
+
if [[ -n "$AGENT_USER" ]]; then
|
|
1555
|
+
AGENT_PSG_HELD=$(pm_agent_user_holds_psg "$ORG_ALIAS")
|
|
1556
|
+
fi
|
|
1557
|
+
[[ -n "$AGENT_USER" && "${AGENT_PSG_HELD:-0}" -ge 1 ]]
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
if check_agent_provisioned; then
|
|
1561
|
+
log_pass "Agent user: $AGENT_USER"
|
|
1562
|
+
log_pass "Agent user holds SalesManagementAgentUserPsg"
|
|
1563
|
+
else
|
|
1564
|
+
echo -n " Waiting for auto-provisioning (up to ${MAX_WAIT_SECONDS}s)"
|
|
1565
|
+
WAITED=0
|
|
1566
|
+
while [[ $WAITED -lt $MAX_WAIT_SECONDS ]]; do
|
|
1567
|
+
if check_agent_provisioned; then
|
|
1568
|
+
echo ""
|
|
1569
|
+
log_pass "Agent user: $AGENT_USER"
|
|
1570
|
+
log_pass "Agent user holds SalesManagementAgentUserPsg"
|
|
1571
|
+
break
|
|
1572
|
+
fi
|
|
1573
|
+
|
|
1574
|
+
echo -n "."
|
|
1575
|
+
sleep "$POLL_INTERVAL"
|
|
1576
|
+
WAITED=$((WAITED + POLL_INTERVAL))
|
|
1577
|
+
done
|
|
1578
|
+
|
|
1579
|
+
if [[ $WAITED -ge $MAX_WAIT_SECONDS ]]; then
|
|
1580
|
+
echo ""
|
|
1581
|
+
# Report what we have
|
|
1582
|
+
if [[ -n "$AGENT_USER" ]]; then
|
|
1583
|
+
log_pass "Agent user: $AGENT_USER"
|
|
1584
|
+
[[ "${AGENT_PSG_HELD:-0}" -ge 1 ]] && log_pass "Agent user holds SalesManagementAgentUserPsg" || log_warn "Agent user does not yet hold SalesManagementAgentUserPsg"
|
|
1585
|
+
else
|
|
1586
|
+
log_warn "Agent user: not yet provisioned"
|
|
1587
|
+
fi
|
|
1588
|
+
echo ""
|
|
1589
|
+
echo " Provisioning may still be in progress. Continuing..."
|
|
1590
|
+
echo " (Re-run verify-all.sh after 5 minutes to confirm.)"
|
|
1591
|
+
fi
|
|
1592
|
+
fi
|
|
1593
|
+
|
|
1594
|
+
# ------------------------------------------------------------
|
|
1595
|
+
# Phase 4a: Report recalculation Status for BOTH PSGs (no waiting)
|
|
1596
|
+
# ------------------------------------------------------------
|
|
1597
|
+
# #10: report the recalculation Status for BOTH SalesManagementUserPsg AND the
|
|
1598
|
+
# autonomous agent user's SalesManagementAgentUserPsg (previously only the former
|
|
1599
|
+
# was surfaced).
|
|
1600
|
+
#
|
|
1601
|
+
# What triggers each PSG's recalc:
|
|
1602
|
+
# - SalesManagementUserPsg: define-agent-access.sh inserts a
|
|
1603
|
+
# PermissionSetGroupComponent (the custom Agent Access permset), which fires
|
|
1604
|
+
# its recalc. That is a CUSTOM addition this skill owns.
|
|
1605
|
+
# - SalesManagementAgentUserPsg: this is a MANAGED PSG owned by the Deal Agent
|
|
1606
|
+
# package. Its components (and thus its recalc) are established by provisioning
|
|
1607
|
+
# (bundle publish / enablement) — we deliberately do NOT insert a component
|
|
1608
|
+
# into it, because writing to a managed PSG's membership is not ours to do and
|
|
1609
|
+
# PermissionSetGroup.Status is not client-writable. So for the agent PSG we
|
|
1610
|
+
# only REPORT Status; a lingering non-'Updated' here points to a provisioning
|
|
1611
|
+
# issue, not something setup can nudge.
|
|
1612
|
+
#
|
|
1613
|
+
# Both recalcs converge asynchronously — we deliberately do NOT block on either
|
|
1614
|
+
# (completion only matters when a user views suggestions in the UI or the
|
|
1615
|
+
# scheduled flow runs). We read Status once for reporting; a 'Failed' Status is
|
|
1616
|
+
# tracked as an issue.
|
|
1617
|
+
for PSG_NAME in SalesManagementUserPsg SalesManagementAgentUserPsg; do
|
|
1618
|
+
PSG_STATE=$(psg_status "$ORG_ALIAS" "$PSG_NAME")
|
|
1619
|
+
case "$PSG_STATE" in
|
|
1620
|
+
Updated) log_pass "PSG '$PSG_NAME' Status: Updated" ;;
|
|
1621
|
+
not-found) log_info "PSG '$PSG_NAME' not present yet (provisioning may still be in progress)" ;;
|
|
1622
|
+
Failed) track_issue "🟡 WARNING" "PSG '$PSG_NAME' recalculation reported Failed" "Permissions from this PSG may be incomplete. Investigate the PSG in Setup and re-run verify-all.sh." ;;
|
|
1623
|
+
*) log_info "PSG '$PSG_NAME' Status: $PSG_STATE — recalculation is async and will complete on its own." ;;
|
|
1624
|
+
esac
|
|
1625
|
+
done
|
|
1626
|
+
|
|
1627
|
+
# ============================================================
|
|
1628
|
+
# Phase 4b: Agent Verification
|
|
1629
|
+
# ============================================================
|
|
1630
|
+
# CRITICAL: A Pipeline Management BotDefinition is REQUIRED for a functional
|
|
1631
|
+
# Pipeline Management agent. Users need an interactive Sales Agent they can
|
|
1632
|
+
# chat with.
|
|
1633
|
+
#
|
|
1634
|
+
# Detection matches the scheduled flow's logic: BotDefinition.AgentTemplate
|
|
1635
|
+
# IN ('SalesMgmt__NGASalesAgent','SalesMgmt__SalesAgent'). We do NOT match
|
|
1636
|
+
# by hardcoded DeveloperName — different orgs publish under different local
|
|
1637
|
+
# names (SalesAgent, Sale_Agent, etc.).
|
|
1638
|
+
#
|
|
1639
|
+
# This phase verifies:
|
|
1640
|
+
# 1. A PM BotDefinition exists (created via authoring bundle)
|
|
1641
|
+
# 2. Agent user + PSG provisioned
|
|
1642
|
+
#
|
|
1643
|
+
# If BotDefinition is missing, it MUST be created. The script exits 1 on failure.
|
|
1644
|
+
# ============================================================
|
|
1645
|
+
|
|
1646
|
+
section "Phase 4b: Agent Architecture Verification"
|
|
1647
|
+
|
|
1648
|
+
# Check 1: Does a PM BotDefinition exist (matched by AgentTemplate)?
|
|
1649
|
+
AGENT_EXISTS=$(pm_bot_count "$ORG_ALIAS")
|
|
1650
|
+
|
|
1651
|
+
if [[ "$AGENT_EXISTS" -ge 1 ]]; then
|
|
1652
|
+
PM_BOT_NAME=$(pm_bot_developer_name "$ORG_ALIAS")
|
|
1653
|
+
log_pass "BotDefinition:${PM_BOT_NAME:-<unknown>} exists (matched by AgentTemplate)"
|
|
1654
|
+
|
|
1655
|
+
# Existence is not enough: auto-provisioning leaves the BotVersion Inactive.
|
|
1656
|
+
# An inactive agent means the scheduled flow never adds the agent user to
|
|
1657
|
+
# opportunity teams (no suggestions) and Agent Access (Phase 4c) can't be
|
|
1658
|
+
# defined. Verify an active version exists and activate the latest if not.
|
|
1659
|
+
BOT_ACTIVE_COUNT=$(pm_bot_active_version_count "$ORG_ALIAS")
|
|
1660
|
+
if [[ "$BOT_ACTIVE_COUNT" -ge 1 ]]; then
|
|
1661
|
+
log_pass "BotDefinition:${PM_BOT_NAME:-<unknown>} has an active version"
|
|
1662
|
+
else
|
|
1663
|
+
log_warn "BotDefinition:${PM_BOT_NAME:-<unknown>} has no active version — activating (auto-provisioned bots land Inactive)..."
|
|
1664
|
+
if activate_pm_bot_if_inactive "$ORG_ALIAS"; then
|
|
1665
|
+
log_pass "BotDefinition:${PM_BOT_NAME:-<unknown>} activated"
|
|
1666
|
+
else
|
|
1667
|
+
track_issue "🔴 BLOCKER" "BotDefinition exists but has no active version and activation failed" "The scheduled flow will not generate suggestions and Agent Access cannot be defined. Activate the Sales Agent (Setup → Agents) or re-run: bash create-agent.sh $ORG_ALIAS"
|
|
1668
|
+
fi
|
|
1669
|
+
fi
|
|
1670
|
+
else
|
|
1671
|
+
# BotDefinition missing - must be created
|
|
1672
|
+
# Re-check agent user (may have been set in Phase 4)
|
|
1673
|
+
AGENT_USER_RECHECK=$(sf data query --query "SELECT Id, Username FROM User WHERE Username LIKE '%salesmanagementagentuser%'" --target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.records[0].Username // empty' 2>/dev/null || echo "")
|
|
1674
|
+
|
|
1675
|
+
if [[ -n "$AGENT_USER_RECHECK" ]]; then
|
|
1676
|
+
# Agent user exists but BotDefinition missing - create it via bundle publish
|
|
1677
|
+
log_warn "No PM BotDefinition (AgentTemplate match) found - attempting to create..."
|
|
1678
|
+
log_pass " Agent user: $AGENT_USER_RECHECK"
|
|
1679
|
+
echo " The agent user + PSGs are sufficient for suggestion generation."
|
|
1680
|
+
echo " The schedule-triggered flow runs as the agent user directly."
|
|
1681
|
+
|
|
1682
|
+
# OPTIONAL: Deploy standalone agent for richer agent experience
|
|
1683
|
+
log_info "Deploying standalone Sales Management Agent for enhanced capabilities..."
|
|
1684
|
+
|
|
1685
|
+
# Use shared library function (returns on failure so setup-all can track issues)
|
|
1686
|
+
if ! publish_and_activate_agent "$ORG_ALIAS" "return" "structured"; then
|
|
1687
|
+
track_issue "🔴 BLOCKER" "Agent bundle publish/activate failed" "A Pipeline Management BotDefinition (AgentTemplate match) is REQUIRED"
|
|
1688
|
+
echo " SETUP FAILED: Cannot proceed without an active Sales Agent."
|
|
1689
|
+
exit 1
|
|
1690
|
+
fi
|
|
1691
|
+
else
|
|
1692
|
+
# Neither BotDefinition nor agent user — genuinely failed provisioning
|
|
1693
|
+
log_warn "No agent user or BotDefinition found after provisioning wait"
|
|
1694
|
+
track_issue "🔴 ERROR" "Agent user not provisioned after enablement" "Attempting SOAP toggle to re-provision agent user + PSGs, then publish authoring bundle for BotDefinition"
|
|
1695
|
+
echo ""
|
|
1696
|
+
echo " Pipeline Management provisioning appears incomplete."
|
|
1697
|
+
echo " This is unusual — the agent user is normally created on enablement."
|
|
1698
|
+
echo ""
|
|
1699
|
+
|
|
1700
|
+
# Only attempt fallback deployment if we have NO agent user at all
|
|
1701
|
+
# --- APPROACH 1: SOAP toggle — re-provisions the agent user and PSGs only.
|
|
1702
|
+
# It does NOT create BotDefinition:SalesAgent; that comes from the
|
|
1703
|
+
# authoring-bundle publish in APPROACH 2 below.
|
|
1704
|
+
log_try "Attempting SOAP toggle to re-provision agent user + PSGs (does NOT create BotDefinition)..."
|
|
1705
|
+
# Disable
|
|
1706
|
+
soap_update "<met:metadata xsi:type='met:SalesDealAgentSettings' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'><met:fullName>SalesDealAgent</met:fullName><met:enableDealAgent>false</met:enableDealAgent></met:metadata>" >/dev/null
|
|
1707
|
+
sleep $SLEEP_TOGGLE_OFF
|
|
1708
|
+
# Re-enable
|
|
1709
|
+
soap_update "<met:metadata xsi:type='met:SalesDealAgentSettings' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'><met:fullName>SalesDealAgent</met:fullName><met:enableDealAgent>true</met:enableDealAgent><met:enableDealAgentAutoApproveAllTasks>false</met:enableDealAgentAutoApproveAllTasks></met:metadata>" >/dev/null
|
|
1710
|
+
sleep $SLEEP_TOGGLE_ON
|
|
1711
|
+
|
|
1712
|
+
# Verify: check for agent user (primary indicator that the toggle took effect)
|
|
1713
|
+
AGENT_USER_RETRY=$(sf data query --query "SELECT Id, Username FROM User WHERE Username LIKE '%salesmanagementagentuser%'" --target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.records[0].Username // empty' 2>/dev/null || echo "")
|
|
1714
|
+
if [[ -n "$AGENT_USER_RETRY" ]]; then
|
|
1715
|
+
log_pass "Agent user + PSGs re-provisioned via SOAP toggle: $AGENT_USER_RETRY"
|
|
1716
|
+
else
|
|
1717
|
+
log_warn "SOAP toggle did not re-provision the agent user"
|
|
1718
|
+
fi
|
|
1719
|
+
|
|
1720
|
+
# --- APPROACH 2: Publish the authoring bundle to create BotDefinition:SalesAgent.
|
|
1721
|
+
# Always attempt this: the toggle above never creates the BotDefinition,
|
|
1722
|
+
# so it must come from the bundle publish regardless of whether the
|
|
1723
|
+
# toggle re-provisioned the agent user.
|
|
1724
|
+
log_try "Publishing authoring bundle to create BotDefinition:SalesAgent..."
|
|
1725
|
+
if publish_and_activate_agent "$ORG_ALIAS" "return" "structured"; then
|
|
1726
|
+
log_pass "BotDefinition:SalesAgent published and activated via authoring bundle"
|
|
1727
|
+
else
|
|
1728
|
+
log_warn "Authoring bundle publish/activate failed"
|
|
1729
|
+
fi
|
|
1730
|
+
|
|
1731
|
+
# Final check
|
|
1732
|
+
FINAL_AGENT_USER=$(sf data query --query "SELECT Id, Username FROM User WHERE Username LIKE '%salesmanagementagentuser%'" --target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.records[0].Username // empty' 2>/dev/null || echo "")
|
|
1733
|
+
if [[ -z "$FINAL_AGENT_USER" ]]; then
|
|
1734
|
+
log_warn "Agent provisioning failed via all approaches"
|
|
1735
|
+
echo " BLOCKER: No agent user found. Suggestions cannot generate without it."
|
|
1736
|
+
echo ""
|
|
1737
|
+
echo " Manual resolution:"
|
|
1738
|
+
echo " 1. Setup → Sales → Pipeline Management → disable and re-enable"
|
|
1739
|
+
echo " 2. Wait 5 minutes for provisioning"
|
|
1740
|
+
echo " 3. Re-run: bash verify-all.sh $ORG_ALIAS"
|
|
1741
|
+
# Recovery exhausted: upgrade the provisional ERROR to a terminal BLOCKER so the
|
|
1742
|
+
# final gate reports SETUP INCOMPLETE / exit 1 instead of a false SETUP COMPLETE.
|
|
1743
|
+
track_issue "🔴 BLOCKER" "Agent user not provisioned after all recovery attempts" "Manually disable/re-enable Pipeline Management (Setup → Sales), wait 5 min, then re-run: bash setup-all.sh $ORG_ALIAS"
|
|
1744
|
+
fi
|
|
1745
|
+
fi
|
|
1746
|
+
fi
|
|
1747
|
+
|
|
1748
|
+
# ============================================================
|
|
1749
|
+
# Phase 4c: Agent Access (W-23242378)
|
|
1750
|
+
# Once a PM BotDefinition exists, add it to "Agent Access" on a custom permset
|
|
1751
|
+
# linked into SalesManagementUserPsg so human users can launch the agent.
|
|
1752
|
+
# define-agent-access.sh is idempotent and exits gracefully if no active bot.
|
|
1753
|
+
# ============================================================
|
|
1754
|
+
section "Phase 4c: Agent Access"
|
|
1755
|
+
|
|
1756
|
+
if [[ -f "$SCRIPT_DIR/define-agent-access.sh" ]]; then
|
|
1757
|
+
bash "$SCRIPT_DIR/define-agent-access.sh" "$ORG_ALIAS" \
|
|
1758
|
+
|| track_issue "🟡 WARNING" "Agent Access definition reported an issue" "Users holding SalesManagementUserPsg may not be able to launch the agent until Agent Access is defined (re-run: bash define-agent-access.sh $ORG_ALIAS)"
|
|
1759
|
+
else
|
|
1760
|
+
log_warn "define-agent-access.sh not found — skipping Agent Access definition"
|
|
1761
|
+
fi
|
|
1762
|
+
|
|
1763
|
+
# ============================================================
|
|
1764
|
+
# Phase 4c.5: Stage Descriptions (BEFORE the StageName prompt)
|
|
1765
|
+
# ============================================================
|
|
1766
|
+
# Stage descriptions are the grounding source the managed StageName template reads;
|
|
1767
|
+
# they MUST exist before Phase 4d deploys/activates and Phase 4e tests that prompt,
|
|
1768
|
+
# otherwise a StageName-selected run would verify a prompt that can generate nothing.
|
|
1769
|
+
# run_stage_descriptions() self-gates on STAGENAME_SELECTED (no-op otherwise) and
|
|
1770
|
+
# sets the script-global STAGE_DESCRIPTIONS_BLOCKED for the Phase 8.5 summary. This
|
|
1771
|
+
# lives in Block A, so on a --from-phase flow (Call 2) run it is skipped entirely —
|
|
1772
|
+
# by then Call 1 has already created the descriptions.
|
|
1773
|
+
run_stage_descriptions
|
|
1774
|
+
|
|
1775
|
+
# ============================================================
|
|
1776
|
+
# Phase 4d: Prompt Templates for Selected Fields
|
|
1777
|
+
# ============================================================
|
|
1778
|
+
# 3-phase setup: create/activate the field-completion prompt templates BEFORE
|
|
1779
|
+
# the flow goes live, so the verification gate (Phase 4e) can test them and the
|
|
1780
|
+
# admin can confirm output before any scheduled run. Delegates to
|
|
1781
|
+
# add-field-suggestion.sh --skip-flow (the flow wiring is owned by Phase 5).
|
|
1782
|
+
# - StageName: picklist, uses the managed template — nothing to deploy.
|
|
1783
|
+
# - NextStep : has a curated OOTB managed prompt. Deploy an OVERRIDE only when
|
|
1784
|
+
# the admin supplied a goal/instruction; otherwise leave the OOTB prompt.
|
|
1785
|
+
# - Custom fields: always deploy a new field-completion template (with the
|
|
1786
|
+
# admin's goal/instruction, or add-field-suggestion.sh's defaults).
|
|
1787
|
+
section "Phase 4d: Prompt Templates for Selected Fields"
|
|
1788
|
+
|
|
1789
|
+
declare -a DEPLOYED_TEMPLATE_FIELDS=()
|
|
1790
|
+
for _f in "${FIELD_ARRAY[@]}"; do
|
|
1791
|
+
if [[ "$_f" == "StageName" ]]; then
|
|
1792
|
+
log_info "StageName: uses the managed picklist template (nothing to deploy)."
|
|
1793
|
+
continue
|
|
1794
|
+
fi
|
|
1795
|
+
_goal="${FIELD_PROMPTS["${_f}_goal"]:-}"
|
|
1796
|
+
_instr="${FIELD_PROMPTS["${_f}_instruction"]:-}"
|
|
1797
|
+
if [[ "$_f" == "NextStep" && -z "$_goal" && -z "$_instr" ]]; then
|
|
1798
|
+
log_info "NextStep: keeping the OOTB managed prompt (no customization requested)."
|
|
1799
|
+
DEPLOYED_TEMPLATE_FIELDS+=("$_f")
|
|
1800
|
+
continue
|
|
1801
|
+
fi
|
|
1802
|
+
|
|
1803
|
+
# Build the add-field-suggestion.sh argument list safely (array, not eval).
|
|
1804
|
+
_afs_args=("$ORG_ALIAS" "$_f" --skip-flow)
|
|
1805
|
+
[[ -n "$_goal" ]] && _afs_args+=(--goal "$_goal")
|
|
1806
|
+
[[ -n "$_instr" ]] && _afs_args+=(--instruction "$_instr")
|
|
1807
|
+
|
|
1808
|
+
log_try "Deploying prompt template for '$_f'..."
|
|
1809
|
+
if bash "$SCRIPT_DIR/add-field-suggestion.sh" "${_afs_args[@]}"; then
|
|
1810
|
+
log_pass "Prompt template ready for '$_f'."
|
|
1811
|
+
DEPLOYED_TEMPLATE_FIELDS+=("$_f")
|
|
1812
|
+
else
|
|
1813
|
+
log_warn "Prompt template deploy failed for '$_f' — suggestions for this field may not generate."
|
|
1814
|
+
track_issue "🟡 WARNING" "Prompt template deploy failed for field '$_f'" "Re-run: bash add-field-suggestion.sh $ORG_ALIAS $_f"
|
|
1815
|
+
fi
|
|
1816
|
+
done
|
|
1817
|
+
|
|
1818
|
+
# --- Prune fields whose template deploy failed, BEFORE the flow is built ---
|
|
1819
|
+
# A field with no active template that is still wired into the flow produces zero
|
|
1820
|
+
# suggestions at runtime — the exact silent failure this 3-phase refactor exists to
|
|
1821
|
+
# prevent. The wired set must be: successfully-deployed fields + StageName (managed
|
|
1822
|
+
# picklist template, nothing to deploy). Rebuild SELECTED_FIELDS/FIELD_ARRAY from
|
|
1823
|
+
# those survivors so Phase 5 wires only fields that will actually generate.
|
|
1824
|
+
_WIRE_FIELDS=("${DEPLOYED_TEMPLATE_FIELDS[@]}")
|
|
1825
|
+
if [[ "$STAGENAME_SELECTED" == "true" ]]; then
|
|
1826
|
+
_WIRE_FIELDS+=("StageName")
|
|
1827
|
+
fi
|
|
1828
|
+
if [[ ${#_WIRE_FIELDS[@]} -lt ${#FIELD_ARRAY[@]} ]]; then
|
|
1829
|
+
# At least one selected field was dropped (template deploy failed).
|
|
1830
|
+
_dropped=()
|
|
1831
|
+
for _sf in "${FIELD_ARRAY[@]}"; do
|
|
1832
|
+
_keep=false
|
|
1833
|
+
for _wf in "${_WIRE_FIELDS[@]}"; do [[ "$_sf" == "$_wf" ]] && { _keep=true; break; }; done
|
|
1834
|
+
[[ "$_keep" == "false" ]] && _dropped+=("$_sf")
|
|
1835
|
+
done
|
|
1836
|
+
log_warn "Dropping field(s) with no active template from the flow: ${_dropped[*]}"
|
|
1837
|
+
fi
|
|
1838
|
+
# Abort if every field fell out — a flow with an empty OpportunityFields collection
|
|
1839
|
+
# would be a no-op, and this preserves the up-front abort-on-zero invariant.
|
|
1840
|
+
if [[ ${#_WIRE_FIELDS[@]} -eq 0 ]]; then
|
|
1841
|
+
log_fail "No field has an active prompt template — cannot build a working flow."
|
|
1842
|
+
echo " Every selected field's template failed to deploy in Phase 4d (see warnings above)."
|
|
1843
|
+
echo " Fix the template deploy(s), then re-run setup."
|
|
1844
|
+
exit 1
|
|
1845
|
+
fi
|
|
1846
|
+
FIELD_ARRAY=("${_WIRE_FIELDS[@]}")
|
|
1847
|
+
SELECTED_FIELDS=$(IFS=,; echo "${_WIRE_FIELDS[*]}")
|
|
1848
|
+
|
|
1849
|
+
# Fields to VERIFY (Phase 4e) — a superset of DEPLOYED_TEMPLATE_FIELDS. StageName
|
|
1850
|
+
# deploys nothing (managed picklist template) so it's absent from the deployed set,
|
|
1851
|
+
# but it still generates a default suggestion the admin should see and get a chance
|
|
1852
|
+
# to override. Same for NextStep left uncustomized (OOTB prompt) — it's already in
|
|
1853
|
+
# the deployed set. So: show a default suggestion for every SELECTED field, even
|
|
1854
|
+
# uncustomized ones ("only when selected" — never pull in unselected fields).
|
|
1855
|
+
declare -a VERIFY_FIELDS=("${DEPLOYED_TEMPLATE_FIELDS[@]}")
|
|
1856
|
+
if [[ "$STAGENAME_SELECTED" == "true" ]]; then
|
|
1857
|
+
VERIFY_FIELDS+=("StageName")
|
|
1858
|
+
fi
|
|
1859
|
+
|
|
1860
|
+
# --- --through-phase prompts: stop here, hand off to the agent tune-loop ---
|
|
1861
|
+
# The templates are deployed and active but the flow is NOT built. The agent now
|
|
1862
|
+
# runs the interactive per-field tune-loop (generate a tailored note → seed → run
|
|
1863
|
+
# /generations → show the suggestion → loop until the user approves), then resumes
|
|
1864
|
+
# with `--from-phase flow`. We STOP BEFORE the built-in Phase 4e gate because the
|
|
1865
|
+
# agent's loop supersedes it (and a piped-stdin run couldn't block on it anyway).
|
|
1866
|
+
if [[ "$RUN_THROUGH_PHASE" == "prompts" ]]; then
|
|
1867
|
+
section "Prompt templates ready — handing off for tuning"
|
|
1868
|
+
log_pass "Deployed/active prompt template field(s): ${DEPLOYED_TEMPLATE_FIELDS[*]:-none}"
|
|
1869
|
+
if [[ "$STAGENAME_SELECTED" == "true" ]]; then
|
|
1870
|
+
log_info "StageName is also selected (managed picklist template; no per-field tuning)."
|
|
1871
|
+
fi
|
|
1872
|
+
echo ""
|
|
1873
|
+
echo " NEXT (agent tune-loop): for each CUSTOMIZED field, seed a tailored note and test:"
|
|
1874
|
+
echo " bash add-field-suggestion.sh $ORG_ALIAS <Field> --skip-flow --force \\"
|
|
1875
|
+
echo " --goal '<goal>' --instruction '<instruction>' \\"
|
|
1876
|
+
echo " --verify-with-note --note '<note tailored to the instruction>'"
|
|
1877
|
+
echo ""
|
|
1878
|
+
echo " ALSO show the DEFAULT suggestion for every selected field left UNCUSTOMIZED"
|
|
1879
|
+
echo " (StageName always; NextStep when no goal/instruction was given) so the user"
|
|
1880
|
+
echo " can decide whether to keep or change the default even though they didn't tune it:"
|
|
1881
|
+
echo " bash verify-prompt-generation.sh $ORG_ALIAS <Field> <oppId>"
|
|
1882
|
+
echo " To CHANGE it: NextStep is tunable via add-field-suggestion.sh (above);"
|
|
1883
|
+
echo " StageName's suggestion is shaped by its stage descriptions — re-run with"
|
|
1884
|
+
echo " --create-stage-descriptions (or edit OpptStageDescription) to change it."
|
|
1885
|
+
echo ""
|
|
1886
|
+
echo " Loop until the user approves each suggestion, then build & activate the flow:"
|
|
1887
|
+
echo " bash setup-all.sh $ORG_ALIAS --fields \"$SELECTED_FIELDS\" --from-phase flow"
|
|
1888
|
+
echo ""
|
|
1889
|
+
log_info "Stopping after Phase 4d (--through-phase prompts). Flow NOT yet built."
|
|
1890
|
+
exit 0
|
|
1891
|
+
fi
|
|
1892
|
+
|
|
1893
|
+
# ============================================================
|
|
1894
|
+
# Phase 4e: Prompt Verification Gate (before the flow goes live)
|
|
1895
|
+
# ============================================================
|
|
1896
|
+
# 3-phase setup: the templates from Phase 4d are now active but the flow is NOT
|
|
1897
|
+
# yet deployed/activated. Test each template SYNCHRONOUSLY against a real
|
|
1898
|
+
# opportunity + a seeded note, show the admin exactly what the AI will suggest,
|
|
1899
|
+
# and (interactively) gate flow activation on their approval. This catches a bad
|
|
1900
|
+
# goal/instruction BEFORE the scheduled run ever fires — far cheaper to fix now
|
|
1901
|
+
# than after go-live.
|
|
1902
|
+
#
|
|
1903
|
+
# We verify VERIFY_FIELDS — every SELECTED field that generates a suggestion,
|
|
1904
|
+
# including ones left uncustomized (NextStep on its OOTB prompt, and StageName on
|
|
1905
|
+
# the managed picklist template). Showing those defaults lets the admin override a
|
|
1906
|
+
# default they never explicitly tuned. StageName generates only because Phase 4c.5
|
|
1907
|
+
# already created its grounding stage descriptions.
|
|
1908
|
+
#
|
|
1909
|
+
# Fully skipped when:
|
|
1910
|
+
# - --skip-prompt-verification was passed (explicit opt-out), OR
|
|
1911
|
+
# - no selected field generates a suggestion (VERIFY_FIELDS empty).
|
|
1912
|
+
# In NON_INTERACTIVE runs (incl. agent-driven — a piped stdin forces it) the
|
|
1913
|
+
# generation still RUNS and the suggestions are PRINTED for review; only the human
|
|
1914
|
+
# yes/no gate degrades to informational (the flow proceeds without blocking).
|
|
1915
|
+
section "Phase 4e: Prompt Verification Gate"
|
|
1916
|
+
|
|
1917
|
+
if [[ "$SKIP_PROMPT_VERIFICATION" == "true" ]]; then
|
|
1918
|
+
log_info "Prompt verification skipped (--skip-prompt-verification)."
|
|
1919
|
+
elif [[ ${#VERIFY_FIELDS[@]} -eq 0 ]]; then
|
|
1920
|
+
log_info "No selected field generates a suggestion to verify."
|
|
1921
|
+
else
|
|
1922
|
+
# NOTE ON NON-INTERACTIVE RUNS (incl. agent-driven, since a piped stdin forces
|
|
1923
|
+
# NON_INTERACTIVE): we STILL seed a test opp/note and run /generations below so
|
|
1924
|
+
# the generated suggestions are printed for review. What a non-interactive run
|
|
1925
|
+
# cannot do is BLOCK on a human yes/no — so the approval gate at the end degrades
|
|
1926
|
+
# to informational output and the flow proceeds. An agent relays this output to
|
|
1927
|
+
# the user; a true unattended run (cron/CI) simply logs it.
|
|
1928
|
+
# --- Find an eligible open opportunity (matches the flow's own filter) ---
|
|
1929
|
+
# IsClosed=false AND CloseDate within +90 days, so we don't test against an opp
|
|
1930
|
+
# the scheduled flow would itself skip (which would look like a false negative).
|
|
1931
|
+
VERIFY_OPP_ID=$(sf data query -q "SELECT Id FROM Opportunity WHERE IsClosed=false AND CloseDate>=TODAY AND CloseDate<=NEXT_N_DAYS:90 ORDER BY LastModifiedDate DESC LIMIT 1" --target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.records[0].Id // empty')
|
|
1932
|
+
|
|
1933
|
+
if [[ -z "$VERIFY_OPP_ID" ]]; then
|
|
1934
|
+
log_try "No eligible open opportunity found — creating one for verification."
|
|
1935
|
+
# A bare Name + future CloseDate + open Stage satisfies the flow's eligibility
|
|
1936
|
+
# filter. Don't hardcode 'Prospecting' — orgs with custom Sales Processes/record
|
|
1937
|
+
# types may not have it. Query the first active, open (non-won/lost) stage.
|
|
1938
|
+
_open_stage=$(sf data query -q "SELECT MasterLabel FROM OpportunityStage WHERE IsActive=true AND IsClosed=false ORDER BY SortOrder LIMIT 1" --target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.records[0].MasterLabel // empty')
|
|
1939
|
+
[[ -z "$_open_stage" ]] && _open_stage="Prospecting" # last-resort default
|
|
1940
|
+
VERIFY_OPP_ID=$(sf data create record --sobject Opportunity \
|
|
1941
|
+
--values "Name='PM Setup Verification Opp' StageName='${_open_stage}' CloseDate=$(python3 -c 'import datetime; print((datetime.date.today()+datetime.timedelta(days=30)).isoformat())')" \
|
|
1942
|
+
--target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.id // empty')
|
|
1943
|
+
if [[ -n "$VERIFY_OPP_ID" ]]; then
|
|
1944
|
+
log_pass "Created verification opportunity $VERIFY_OPP_ID"
|
|
1945
|
+
else
|
|
1946
|
+
log_warn "Could not create a verification opportunity; skipping the prompt gate."
|
|
1947
|
+
track_issue "🟡 WARNING" "Prompt verification skipped — no eligible opportunity" "Create an open Opportunity (CloseDate within 90 days) and test with verify-prompt-generation.sh."
|
|
1948
|
+
VERIFY_OPP_ID=""
|
|
1949
|
+
fi
|
|
1950
|
+
else
|
|
1951
|
+
log_pass "Using eligible opportunity $VERIFY_OPP_ID for verification."
|
|
1952
|
+
fi
|
|
1953
|
+
|
|
1954
|
+
if [[ -n "$VERIFY_OPP_ID" ]]; then
|
|
1955
|
+
# --- Seed a ContentNote with sample conversation signal + link to the opp ---
|
|
1956
|
+
# Interpolate relative dates (today / today+9d) so the sample never reads as
|
|
1957
|
+
# stale to the model on a run weeks after this was authored.
|
|
1958
|
+
_note_today=$(python3 -c 'import datetime; print(datetime.date.today().strftime("%B %-d"))')
|
|
1959
|
+
_note_soon=$(python3 -c 'import datetime; print((datetime.date.today()+datetime.timedelta(days=9)).strftime("%B %-d"))')
|
|
1960
|
+
NOTE_BODY="Call with John Smith on ${_note_today}: Discussed Q4 budget approval. John mentioned competitor Acme Corp is being evaluated. Next step: Schedule executive sponsor meeting by ${_note_soon}. Risk: Budget freeze possible if exec sponsor changes."
|
|
1961
|
+
# macOS/BSD `base64` line-wraps at 76 cols (embeds newlines); GNU's `-w0` isn't
|
|
1962
|
+
# portable. `tr -d '\n'` strips wraps on both so the value stays single-line and
|
|
1963
|
+
# doesn't break the --values string below.
|
|
1964
|
+
NOTE_B64=$(printf '%s' "$NOTE_BODY" | base64 | tr -d '\n')
|
|
1965
|
+
log_try "Seeding a ContentNote with sample conversation data."
|
|
1966
|
+
# Note-seeding is best-effort: `|| true` keeps a create failure from tripping
|
|
1967
|
+
# `set -e` so it degrades to the log_warn fallback instead of aborting the gate.
|
|
1968
|
+
VERIFY_NOTE_ID=$(sf data create record --sobject ContentNote --values "Title='PM Setup Verification Note' Content='${NOTE_B64}'" --target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.id // empty' || true)
|
|
1969
|
+
if [[ -n "$VERIFY_NOTE_ID" ]]; then
|
|
1970
|
+
# ContentNote Id IS the ContentDocumentId (shared 069 prefix); link directly.
|
|
1971
|
+
# The ContentVersion→LatestVersionId subquery returns null on some orgs.
|
|
1972
|
+
if sf data create record --sobject ContentDocumentLink --values "ContentDocumentId='${VERIFY_NOTE_ID}' LinkedEntityId='${VERIFY_OPP_ID}' ShareType='V' Visibility='AllUsers'" --target-org "$ORG_ALIAS" --json 2>/dev/null >/dev/null; then
|
|
1973
|
+
log_pass "Seeded note and linked to $VERIFY_OPP_ID"
|
|
1974
|
+
else
|
|
1975
|
+
log_warn "Note created but could not link to the opportunity; generation runs against existing grounding data only."
|
|
1976
|
+
fi
|
|
1977
|
+
else
|
|
1978
|
+
log_warn "Could not seed a note; generation runs against existing grounding data only."
|
|
1979
|
+
fi
|
|
1980
|
+
|
|
1981
|
+
# --- Generate a suggestion per verified field and show it to the admin ---
|
|
1982
|
+
echo ""
|
|
1983
|
+
echo " Testing each prompt against opportunity $VERIFY_OPP_ID:"
|
|
1984
|
+
echo ""
|
|
1985
|
+
_gen_ok=0 # count templates that returned usable output
|
|
1986
|
+
for _vf in "${VERIFY_FIELDS[@]}"; do
|
|
1987
|
+
# Resolve the template name the /generations endpoint expects:
|
|
1988
|
+
# - StageName: the managed picklist template (nothing deployed, but it still
|
|
1989
|
+
# generates a default the admin should see and can choose to override).
|
|
1990
|
+
# - NextStep: the managed template (an override folds into it as a new
|
|
1991
|
+
# version; the OOTB prompt shares the same name), so always query the
|
|
1992
|
+
# managed name.
|
|
1993
|
+
# - Custom field: Recommend<Field, stripped of __c and non-alnum>forOpp,
|
|
1994
|
+
# matching add-field-suggestion.sh's dev-name derivation.
|
|
1995
|
+
if [[ "$_vf" == "StageName" ]]; then
|
|
1996
|
+
_vtmpl="sales_pipe_mgmt__RecommendStageforOpp"
|
|
1997
|
+
elif [[ "$_vf" == "NextStep" ]]; then
|
|
1998
|
+
_vtmpl="sales_pipe_mgmt__RecommendNextStepforOpp"
|
|
1999
|
+
else
|
|
2000
|
+
_vtmpl="Recommend$(printf '%s' "$_vf" | sed 's/__c$//' | sed 's/[^A-Za-z0-9]//g')forOpp"
|
|
2001
|
+
fi
|
|
2002
|
+
|
|
2003
|
+
# Bound at 120s: generation can legitimately take longer than a plain REST
|
|
2004
|
+
# call (CURL_TIMEOUT=30), but must not hang indefinitely and blow the run's
|
|
2005
|
+
# tool timeout. A timeout yields non-zero → the `|| echo '{}'` fallback, so a
|
|
2006
|
+
# stall reads as "no output" and flows into the blocked-field handling below.
|
|
2007
|
+
_gen=$(with_timeout 120 sf api request rest "/services/data/v${API_VERSION}/einstein/prompt-templates/${_vtmpl}/generations" \
|
|
2008
|
+
--method POST --target-org "$ORG_ALIAS" \
|
|
2009
|
+
--body "{\"isPreview\":false,\"inputParams\":{\"valueMap\":{\"Input:Opportunity\":{\"value\":{\"id\":\"${VERIFY_OPP_ID}\"}}}},\"additionalConfig\":{\"numGenerations\":1,\"temperature\":0,\"applicationName\":\"PromptTemplateGenerationsInvocable\"}}" 2>/dev/null || echo '{}')
|
|
2010
|
+
_gentext=$(echo "$_gen" | jq -r '.generations[0].text // empty' 2>/dev/null)
|
|
2011
|
+
if [[ -n "$_gentext" ]]; then
|
|
2012
|
+
# The generation text is itself a JSON blob (FieldName/SuggestedNewValue/Reasoning).
|
|
2013
|
+
_sugg=$(echo "$_gentext" | jq -r '.SuggestedNewValue // empty' 2>/dev/null)
|
|
2014
|
+
_reason=$(echo "$_gentext" | jq -r '.Reasoning // empty' 2>/dev/null)
|
|
2015
|
+
_gen_ok=$((_gen_ok + 1))
|
|
2016
|
+
if [[ -n "$_sugg" ]]; then
|
|
2017
|
+
echo " ── $_vf ($_vtmpl)"
|
|
2018
|
+
echo " Suggested: $_sugg"
|
|
2019
|
+
[[ -n "$_reason" ]] && echo " Reasoning: $_reason"
|
|
2020
|
+
else
|
|
2021
|
+
echo " ── $_vf ($_vtmpl)"
|
|
2022
|
+
echo " Raw output: $_gentext"
|
|
2023
|
+
fi
|
|
2024
|
+
else
|
|
2025
|
+
_generr=$(echo "$_gen" | jq -r '.[0].message // .message // empty' 2>/dev/null | head -1)
|
|
2026
|
+
echo " ── $_vf ($_vtmpl)"
|
|
2027
|
+
log_warn "No suggestion generated${_generr:+ — $_generr}"
|
|
2028
|
+
fi
|
|
2029
|
+
echo ""
|
|
2030
|
+
done
|
|
2031
|
+
|
|
2032
|
+
# --- The gate: admin must approve before the flow is deployed/activated ---
|
|
2033
|
+
# If NOT ONE template produced output, the gate tested nothing — approving
|
|
2034
|
+
# "prompts look good" here would be meaningless. Require an explicit,
|
|
2035
|
+
# differently-worded override so a reviewer who saw only warnings cannot
|
|
2036
|
+
# wave through unvalidated prompts on autopilot.
|
|
2037
|
+
if [[ "$NON_INTERACTIVE" == "true" ]]; then
|
|
2038
|
+
# No TTY to block on — the gate degrades to informational output. Report
|
|
2039
|
+
# what was (or wasn't) generated and proceed; the caller (agent/operator)
|
|
2040
|
+
# reviews the printed suggestions and can re-run add-field-suggestion.sh to
|
|
2041
|
+
# adjust a prompt afterward.
|
|
2042
|
+
if [[ "$_gen_ok" -eq 0 ]]; then
|
|
2043
|
+
log_warn "Non-interactive run: NO template produced output — flow will activate on UNVERIFIED prompts."
|
|
2044
|
+
echo " Review the warnings above; adjust with:"
|
|
2045
|
+
echo " bash add-field-suggestion.sh $ORG_ALIAS <Field> --skip-flow --goal '...' --instruction '...' --force"
|
|
2046
|
+
track_issue "🟡 WARNING" "Prompts unverified (non-interactive, no generation output)" "Review suggestions; re-run add-field-suggestion.sh to adjust, then re-run setup."
|
|
2047
|
+
else
|
|
2048
|
+
log_info "Non-interactive run: suggestions shown above are informational — proceeding to flow activation."
|
|
2049
|
+
echo " If a suggestion looks wrong, adjust its prompt with:"
|
|
2050
|
+
echo " bash add-field-suggestion.sh $ORG_ALIAS <Field> --skip-flow --goal '...' --instruction '...' --force"
|
|
2051
|
+
fi
|
|
2052
|
+
elif [[ "$_gen_ok" -eq 0 ]]; then
|
|
2053
|
+
log_warn "No template produced any output — the prompts could NOT be validated."
|
|
2054
|
+
echo ""
|
|
2055
|
+
echo " Common causes: templates still activating (retry in a minute), no"
|
|
2056
|
+
echo " grounding data on the opportunity, or a template deploy that failed above."
|
|
2057
|
+
echo " Proceeding now would activate the flow on UNVERIFIED prompts."
|
|
2058
|
+
echo ""
|
|
2059
|
+
if ! confirm "Proceed to build & activate the flow anyway, WITHOUT verified prompt output?"; then
|
|
2060
|
+
log_fail "Verification could not complete and was not overridden. Flow NOT activated."
|
|
2061
|
+
echo ""
|
|
2062
|
+
echo " The prompt templates are deployed and active, but the suggestion flow"
|
|
2063
|
+
echo " has not been built or activated, so no scheduled suggestions will run."
|
|
2064
|
+
echo " Inspect a prompt with:"
|
|
2065
|
+
echo " bash verify-prompt-generation.sh $ORG_ALIAS <Field>"
|
|
2066
|
+
echo " or adjust one with:"
|
|
2067
|
+
echo " bash add-field-suggestion.sh $ORG_ALIAS <Field> --skip-flow --goal '...' --instruction '...' --force"
|
|
2068
|
+
echo " Then re-run this setup to complete flow activation."
|
|
2069
|
+
exit 1
|
|
2070
|
+
fi
|
|
2071
|
+
log_warn "Proceeding to flow activation on UNVERIFIED prompts (explicit override)."
|
|
2072
|
+
elif ! confirm "Prompts look good — proceed to build & activate the flow?"; then
|
|
2073
|
+
log_fail "Verification declined. Flow NOT activated."
|
|
2074
|
+
echo ""
|
|
2075
|
+
echo " The prompt templates are deployed and active, but the suggestion flow"
|
|
2076
|
+
echo " has not been built or activated, so no scheduled suggestions will run."
|
|
2077
|
+
echo " To adjust a prompt, edit it with:"
|
|
2078
|
+
echo " bash add-field-suggestion.sh $ORG_ALIAS <Field> --skip-flow --goal '...' --instruction '...' --force"
|
|
2079
|
+
echo " Then re-run this setup to complete flow activation."
|
|
2080
|
+
exit 1
|
|
2081
|
+
else
|
|
2082
|
+
log_pass "Verification approved — proceeding to flow activation."
|
|
2083
|
+
fi
|
|
2084
|
+
fi
|
|
2085
|
+
fi
|
|
2086
|
+
|
|
2087
|
+
fi # END BLOCK A (Phase 1.5–4e) — skipped entirely on a --from-phase flow run.
|
|
2088
|
+
|
|
2089
|
+
# ============================================================
|
|
2090
|
+
# BLOCK B — Flow, Agent wiring & Verification (Phase 5 → 8)
|
|
2091
|
+
# ============================================================
|
|
2092
|
+
# Builds the suggestion flow from the approved field set, activates it, wires the
|
|
2093
|
+
# agent/PSG, and runs final verification. Self-contained: everything it needs it
|
|
2094
|
+
# re-derives from the org (bot count, PSG id/state) or from the context vars set in
|
|
2095
|
+
# Phase 0/1/1.7 — so it runs correctly both in a full end-to-end pass and as the
|
|
2096
|
+
# second half of a split (--from-phase flow) run.
|
|
2097
|
+
|
|
2098
|
+
# --- Split-path re-prune: fields must have an ACTIVE prompt template ---
|
|
2099
|
+
# On a full run, Block A already pruned SELECTED_FIELDS/FIELD_ARRAY to fields with
|
|
2100
|
+
# a successfully-deployed active template (lines ~1635-1664). On a --from-phase flow
|
|
2101
|
+
# run Block A is skipped, so SELECTED_FIELDS is still the RAW --fields the agent
|
|
2102
|
+
# passed. Wiring a field with no active template into the flow yields zero
|
|
2103
|
+
# suggestions at runtime — the exact silent failure this refactor prevents. So
|
|
2104
|
+
# re-derive the survivor set here from the org (read-only), mirroring Block A's
|
|
2105
|
+
# invariant: keep StageName unconditionally (managed picklist, nothing to deploy),
|
|
2106
|
+
# keep any other field whose GenAiPromptTemplate is active.
|
|
2107
|
+
if [[ "$RUN_FROM_PHASE" == "flow" ]]; then
|
|
2108
|
+
_ACTIVE_TEMPLATES=$(sf api request rest "/services/data/v${API_VERSION}/einstein/prompt-templates" --target-org "$ORG_ALIAS" 2>/dev/null \
|
|
2109
|
+
| jq -r '.promptRecords[]? | select(.fields.IsActive.value==true or .fields.IsActive.value=="true") | .fields.DeveloperName.value' 2>/dev/null || echo "")
|
|
2110
|
+
_WIRE_FIELDS=()
|
|
2111
|
+
_dropped=()
|
|
2112
|
+
for _sf in "${FIELD_ARRAY[@]}"; do
|
|
2113
|
+
if [[ "$_sf" == "StageName" ]]; then
|
|
2114
|
+
_WIRE_FIELDS+=("StageName")
|
|
2115
|
+
continue
|
|
2116
|
+
fi
|
|
2117
|
+
# Expected dev name: Recommend<Field-without-__c-and-non-alnum>forOpp. Match with
|
|
2118
|
+
# or without the sales_pipe_mgmt__ namespace prefix (NextStep's managed template
|
|
2119
|
+
# is namespaced; standalone custom-field templates are not).
|
|
2120
|
+
_clean=$(echo "$_sf" | sed 's/__c$//' | sed 's/[^A-Za-z0-9]//g')
|
|
2121
|
+
_expect="Recommend${_clean}forOpp"
|
|
2122
|
+
if echo "$_ACTIVE_TEMPLATES" | grep -qiE "(^|__)${_expect}\$"; then
|
|
2123
|
+
_WIRE_FIELDS+=("$_sf")
|
|
2124
|
+
else
|
|
2125
|
+
_dropped+=("$_sf")
|
|
2126
|
+
fi
|
|
2127
|
+
done
|
|
2128
|
+
if [[ ${#_dropped[@]} -gt 0 ]]; then
|
|
2129
|
+
log_warn "Dropping field(s) with no active prompt template from the flow: ${_dropped[*]}"
|
|
2130
|
+
echo " These templates were not active on the org. Re-run the tune-loop"
|
|
2131
|
+
echo " (add-field-suggestion.sh ... --force) for each, then re-run --from-phase flow."
|
|
2132
|
+
fi
|
|
2133
|
+
if [[ ${#_WIRE_FIELDS[@]} -eq 0 ]]; then
|
|
2134
|
+
log_fail "No selected field has an active prompt template — cannot build a working flow."
|
|
2135
|
+
echo " Deploy/activate at least one field's template (Call 1 + tune-loop), then re-run --from-phase flow."
|
|
2136
|
+
exit 1
|
|
2137
|
+
fi
|
|
2138
|
+
FIELD_ARRAY=("${_WIRE_FIELDS[@]}")
|
|
2139
|
+
SELECTED_FIELDS=$(IFS=,; echo "${_WIRE_FIELDS[*]}")
|
|
2140
|
+
log_info "Flow will wire ${#FIELD_ARRAY[@]} field(s) with active templates: $SELECTED_FIELDS"
|
|
2141
|
+
fi
|
|
2142
|
+
|
|
2143
|
+
# ============================================================
|
|
2144
|
+
# Phase 5: Schedule-Triggered Flow (with deploy + activation)
|
|
2145
|
+
# ============================================================
|
|
2146
|
+
section "Phase 5: Schedule-Triggered Flow"
|
|
2147
|
+
|
|
2148
|
+
# Detection: check by SourceTemplateId, naming convention, and exact API name
|
|
2149
|
+
FLOW_NAME=""
|
|
2150
|
+
for QUERY in \
|
|
2151
|
+
"SELECT ApiName, IsActive FROM FlowDefinitionView WHERE SourceTemplateId='sales_pipe_mgmt__OppSuggGenSchFlow' AND IsTemplate=false" \
|
|
2152
|
+
"SELECT ApiName, IsActive FROM FlowDefinitionView WHERE ApiName LIKE '%OppSuggGen%' AND IsTemplate=false" \
|
|
2153
|
+
"SELECT ApiName, IsActive FROM FlowDefinitionView WHERE ApiName = 'Process_Field_Update_Suggestions' AND IsTemplate=false"; do
|
|
2154
|
+
|
|
2155
|
+
FLOW_NAME=$(sf data query --query "$QUERY" --target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.records[0].ApiName // empty' 2>/dev/null || echo "")
|
|
2156
|
+
[[ -n "$FLOW_NAME" ]] && break
|
|
2157
|
+
done
|
|
2158
|
+
|
|
2159
|
+
if [[ -n "$FLOW_NAME" ]]; then
|
|
2160
|
+
log_pass "Flow found: $FLOW_NAME"
|
|
2161
|
+
else
|
|
2162
|
+
log_info "Flow not found. Deploying from pre-retrieved template..."
|
|
2163
|
+
|
|
2164
|
+
TEMPLATE_FILE="$SCRIPT_DIR/../assets/pipeline_management_flow.flow-meta.xml"
|
|
2165
|
+
|
|
2166
|
+
if [[ -f "$TEMPLATE_FILE" ]]; then
|
|
2167
|
+
ensure_sfdx_project
|
|
2168
|
+
mkdir -p force-app/main/default/flows
|
|
2169
|
+
FLOW_FILE="force-app/main/default/flows/Process_Field_Update_Suggestions.flow-meta.xml"
|
|
2170
|
+
|
|
2171
|
+
# Greenfield: BUILD the flow with ONLY the up-front-selected fields (no
|
|
2172
|
+
# deploy-then-strip). build_field_collection reads the base template and
|
|
2173
|
+
# writes FLOW_FILE with exactly SELECTED_FIELDS wired into
|
|
2174
|
+
# AddOppFieldsToCollection, emitting a distinct nonzero exit on each failure
|
|
2175
|
+
# (2 empty set / 3 parse-fail / 4 assignment-missing / 5 write-fail).
|
|
2176
|
+
# We do NOT fall back to copying the base template: that verbatim copy wires
|
|
2177
|
+
# BOTH OOTB fields, which would ship StageName the admin excluded (and with no
|
|
2178
|
+
# supporting stage descriptions). The selected set is already validated
|
|
2179
|
+
# non-empty (Phase 1.7) and pruned to fields with active templates, so a build
|
|
2180
|
+
# failure here is a genuine environment problem — abort loudly instead.
|
|
2181
|
+
if build_field_collection "$SELECTED_FIELDS" "$TEMPLATE_FILE" "$FLOW_FILE"; then
|
|
2182
|
+
log_pass "Flow built with ${#FIELD_ARRAY[@]} selected field(s): $SELECTED_FIELDS"
|
|
2183
|
+
else
|
|
2184
|
+
_bfc_rc=$?
|
|
2185
|
+
log_fail "Could not build the flow from selected fields (build_field_collection rc=$_bfc_rc)."
|
|
2186
|
+
case $_bfc_rc in
|
|
2187
|
+
2) echo " Empty field set after pruning — nothing to wire." ;;
|
|
2188
|
+
3) echo " Base flow template did not parse as XML: $TEMPLATE_FILE" ;;
|
|
2189
|
+
4) echo " AddOppFieldsToCollection assignment not found in the base template." ;;
|
|
2190
|
+
5) echo " Could not write the built flow to $FLOW_FILE (permissions/disk)." ;;
|
|
2191
|
+
*) echo " Unexpected failure; check that python3 is on PATH." ;;
|
|
2192
|
+
esac
|
|
2193
|
+
echo " Flow NOT deployed. Fix the above and re-run setup."
|
|
2194
|
+
exit 1
|
|
2195
|
+
fi
|
|
2196
|
+
|
|
2197
|
+
# Update startDate to today
|
|
2198
|
+
TODAY=$(date +%Y-%m-%d)
|
|
2199
|
+
sed_inplace "s|<startDate>[^<]*</startDate>|<startDate>${TODAY}</startDate>|g" "$FLOW_FILE"
|
|
2200
|
+
|
|
2201
|
+
# Sync the flow's SalesManagementAgentTemplate constant with the actual
|
|
2202
|
+
# AgentTemplate of the bot deployed to this org. The template's default
|
|
2203
|
+
# value is the legacy SalesMgmt__SalesAgent, but our authoring bundle
|
|
2204
|
+
# publishes the NGA variant (SalesMgmt__NGASalesAgent). The flow's
|
|
2205
|
+
# BotDefinition lookup matches on this constant, so a mismatch means
|
|
2206
|
+
# the action silently fails to find the agent at runtime.
|
|
2207
|
+
set +e # Allow pm_bot_agent_template to return non-zero without aborting
|
|
2208
|
+
DETECTED_TEMPLATE=$(pm_bot_agent_template "$ORG_ALIAS")
|
|
2209
|
+
DETECT_RC=$?
|
|
2210
|
+
set -e
|
|
2211
|
+
case $DETECT_RC in
|
|
2212
|
+
0)
|
|
2213
|
+
PM_BOT_TOTAL=$(pm_bot_count "$ORG_ALIAS")
|
|
2214
|
+
if [[ "${PM_BOT_TOTAL:-0}" -gt 1 ]]; then
|
|
2215
|
+
log_warn "Multiple active Pipeline Management bots detected on org (${PM_BOT_TOTAL}). Flow will bind to: $DETECTED_TEMPLATE (NGA preferred). Clean up stale bots to avoid runtime ambiguity."
|
|
2216
|
+
fi
|
|
2217
|
+
sed_inplace "s|<stringValue>SalesMgmt__SalesAgent</stringValue>|<stringValue>${DETECTED_TEMPLATE}</stringValue>|g" "$FLOW_FILE"
|
|
2218
|
+
log_info "Flow template constant set to: $DETECTED_TEMPLATE"
|
|
2219
|
+
;;
|
|
2220
|
+
1)
|
|
2221
|
+
log_fail "No active Pipeline Management bot found on org. Phase 4b should have created and activated the bot before flow deploy. Aborting flow deploy to prevent binding the flow to a missing or inactive agent."
|
|
2222
|
+
exit 1
|
|
2223
|
+
;;
|
|
2224
|
+
*)
|
|
2225
|
+
log_fail "Could not query BotDefinition for AgentTemplate (sf data query failed — auth, network, or API error). Re-run after resolving the underlying issue."
|
|
2226
|
+
exit 1
|
|
2227
|
+
;;
|
|
2228
|
+
esac
|
|
2229
|
+
|
|
2230
|
+
# Field wiring for the greenfield flow is already done: build_field_collection
|
|
2231
|
+
# (above) wrote FLOW_FILE with exactly the up-front-selected fields. There is
|
|
2232
|
+
# no deploy-then-strip here. strip_flow_field / add_flow_field (from
|
|
2233
|
+
# shared/flow-builder.sh) are reserved for the REPAIR path below, which reconciles
|
|
2234
|
+
# an already-deployed flow's wired fields to the selected set.
|
|
2235
|
+
|
|
2236
|
+
# --- APPROACH 1: Deploy as source ---
|
|
2237
|
+
log_try "Deploying flow via source deploy..."
|
|
2238
|
+
DEPLOY_RESULT=$(sf project deploy start --source-dir "$FLOW_FILE" --target-org "$ORG_ALIAS" --json 2>/dev/null || echo '{"status":1}')
|
|
2239
|
+
|
|
2240
|
+
if echo "$DEPLOY_RESULT" | jq -e '.status == 0' >/dev/null 2>&1; then
|
|
2241
|
+
FLOW_NAME="Process_Field_Update_Suggestions"
|
|
2242
|
+
log_pass "Flow deployed: $FLOW_NAME"
|
|
2243
|
+
else
|
|
2244
|
+
DEPLOY_ERR=$(safe_jq "$DEPLOY_RESULT" '.result.details.componentFailures[0].problem // .message // "unknown"' "unknown")
|
|
2245
|
+
echo " Source deploy error: $DEPLOY_ERR"
|
|
2246
|
+
|
|
2247
|
+
# --- APPROACH 2: Deploy as metadata ---
|
|
2248
|
+
log_try "Retrying as metadata deploy..."
|
|
2249
|
+
DEPLOY_RESULT=$(sf project deploy start --metadata "Flow:Process_Field_Update_Suggestions" --target-org "$ORG_ALIAS" --json 2>/dev/null || echo '{"status":1}')
|
|
2250
|
+
if echo "$DEPLOY_RESULT" | jq -e '.status == 0' >/dev/null 2>&1; then
|
|
2251
|
+
FLOW_NAME="Process_Field_Update_Suggestions"
|
|
2252
|
+
log_pass "Flow deployed: $FLOW_NAME (metadata approach)"
|
|
2253
|
+
else
|
|
2254
|
+
log_warn "Both deploy approaches failed."
|
|
2255
|
+
fi
|
|
2256
|
+
fi
|
|
2257
|
+
else
|
|
2258
|
+
log_warn "Template file not found at: $TEMPLATE_FILE"
|
|
2259
|
+
fi
|
|
2260
|
+
|
|
2261
|
+
# --- If still no flow, UI guidance ---
|
|
2262
|
+
if [[ -z "$FLOW_NAME" ]]; then
|
|
2263
|
+
log_warn "Flow requires manual creation via Setup UI."
|
|
2264
|
+
echo ""
|
|
2265
|
+
echo " Steps:"
|
|
2266
|
+
echo " 1. Open: ${INSTANCE_URL}/lightning/setup/Flows/home"
|
|
2267
|
+
echo " 2. Find 'OppSuggGenSchFlow' (template, sales_pipe_mgmt namespace)"
|
|
2268
|
+
echo " 3. Click 'Save As' → Label: 'Process Field Update Suggestions'"
|
|
2269
|
+
echo " 4. Click 'Activate'"
|
|
2270
|
+
echo ""
|
|
2271
|
+
|
|
2272
|
+
# Non-interactive: skip (don't block on user input in agent context)
|
|
2273
|
+
if [[ -t 0 ]]; then
|
|
2274
|
+
while true; do
|
|
2275
|
+
read -rp " Press Enter after completing (or 'skip' to continue): " RESPONSE
|
|
2276
|
+
if [[ "$RESPONSE" == "skip" ]]; then break; fi
|
|
2277
|
+
FLOW_NAME=$(sf data query --query "SELECT ApiName FROM FlowDefinitionView WHERE SourceTemplateId='sales_pipe_mgmt__OppSuggGenSchFlow' AND IsTemplate=false" --target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.records[0].ApiName // empty' 2>/dev/null || echo "")
|
|
2278
|
+
[[ -n "$FLOW_NAME" ]] && { log_pass "Flow detected: $FLOW_NAME"; break; }
|
|
2279
|
+
log_warn "Flow not detected yet."
|
|
2280
|
+
done
|
|
2281
|
+
fi
|
|
2282
|
+
fi
|
|
2283
|
+
fi
|
|
2284
|
+
|
|
2285
|
+
# --- Flow Activation + Field Reconciliation (REPAIR path for an existing flow) ---
|
|
2286
|
+
# When the flow already exists we do NOT rebuild it from the base template (that
|
|
2287
|
+
# would discard the org-specific SalesManagementAgentTemplate constant the
|
|
2288
|
+
# deployed flow carries). Instead we RECONCILE the flow's wired fields to the
|
|
2289
|
+
# up-front selected set — the "fix method": strip fields no longer selected,
|
|
2290
|
+
# add newly-selected ones (via strip_flow_field / add_flow_field). We then set
|
|
2291
|
+
# it Active and redeploy. This runs whether or not the flow is currently active,
|
|
2292
|
+
# because the field set may have changed even on an active flow.
|
|
2293
|
+
if [[ -n "$FLOW_NAME" ]]; then
|
|
2294
|
+
FLOW_ACTIVE=$(sf data query --query "SELECT IsActive FROM FlowDefinitionView WHERE ApiName='${FLOW_NAME}'" --target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.records[0].IsActive // "false"' 2>/dev/null || echo "false")
|
|
2295
|
+
|
|
2296
|
+
log_try "Reconciling existing flow to selected fields + activating..."
|
|
2297
|
+
|
|
2298
|
+
# Retrieve → reconcile fields → set Active → reconcile constant → deploy
|
|
2299
|
+
ensure_sfdx_project
|
|
2300
|
+
mkdir -p force-app/main/default/flows
|
|
2301
|
+
sf project retrieve start --metadata "Flow:${FLOW_NAME}" --target-org "$ORG_ALIAS" --json 2>/dev/null >/dev/null || true
|
|
2302
|
+
|
|
2303
|
+
FLOW_FILE="force-app/main/default/flows/${FLOW_NAME}.flow-meta.xml"
|
|
2304
|
+
if [[ -f "$FLOW_FILE" ]]; then
|
|
2305
|
+
# --- Field reconciliation: make the wired set == SELECTED_FIELDS ---
|
|
2306
|
+
# Strip any wired field not in the selected set; add any selected field
|
|
2307
|
+
# not yet wired. Both helpers are idempotent and namespace-safe (python3).
|
|
2308
|
+
# The AddOppFieldsToCollection assignment must never end up empty — the
|
|
2309
|
+
# selected set is guaranteed non-empty (validated in Phase 1.7), so as long
|
|
2310
|
+
# as we add-before-we-finish the assignment stays valid.
|
|
2311
|
+
FIELDS_CHANGED=false
|
|
2312
|
+
WIRED_FIELDS="$(flow_wired_fields "$FLOW_FILE" 2>/dev/null || echo "")"
|
|
2313
|
+
# Normalized ",a,b,c," form computed once for exact-comma membership tests
|
|
2314
|
+
# (mirrors the ,$SELECTED_FIELDS, idiom on the strip side).
|
|
2315
|
+
WIRED_CSV=",$(printf '%s' "$WIRED_FIELDS" | tr '\n' ',' | sed 's/,,*/,/g'),"
|
|
2316
|
+
if [[ -n "$WIRED_FIELDS" ]]; then
|
|
2317
|
+
while IFS= read -r _wired; do
|
|
2318
|
+
[[ -z "$_wired" ]] && continue
|
|
2319
|
+
if [[ ",$SELECTED_FIELDS," != *",$_wired,"* ]]; then
|
|
2320
|
+
# strip: rc 0 = removed, 2 = not present (benign), other = real error.
|
|
2321
|
+
_rc=0; strip_flow_field "$FLOW_FILE" "$_wired" || _rc=$?
|
|
2322
|
+
case $_rc in
|
|
2323
|
+
0) FIELDS_CHANGED=true; log_info "Unwired field no longer selected: $_wired" ;;
|
|
2324
|
+
2) : ;; # not present — nothing to do
|
|
2325
|
+
*) log_warn "Could not unwire '$_wired' (rc=$_rc) — it may remain in the flow."
|
|
2326
|
+
track_issue "🟡 WARNING" "Flow field reconcile: strip of '$_wired' failed (rc=$_rc)" "The flow's wired field set may not match the selected set. Re-run setup or edit the flow manually." ;;
|
|
2327
|
+
esac
|
|
2328
|
+
fi
|
|
2329
|
+
done <<< "$WIRED_FIELDS"
|
|
2330
|
+
fi
|
|
2331
|
+
for _sel in "${FIELD_ARRAY[@]}"; do
|
|
2332
|
+
if [[ "$WIRED_CSV" != *",$_sel,"* ]]; then
|
|
2333
|
+
# add: rc 0 = added, 2 = already wired (benign), other = real error.
|
|
2334
|
+
_rc=0; add_flow_field "$FLOW_FILE" "$_sel" || _rc=$?
|
|
2335
|
+
case $_rc in
|
|
2336
|
+
0) FIELDS_CHANGED=true; log_info "Wired newly-selected field: $_sel" ;;
|
|
2337
|
+
2) : ;; # already wired — nothing to do
|
|
2338
|
+
*) log_warn "Could not wire '$_sel' (rc=$_rc) — it may not generate suggestions."
|
|
2339
|
+
track_issue "🟡 WARNING" "Flow field reconcile: add of '$_sel' failed (rc=$_rc)" "The selected field '$_sel' may not be wired into the flow. Re-run: bash add-field-suggestion.sh $ORG_ALIAS $_sel" ;;
|
|
2340
|
+
esac
|
|
2341
|
+
fi
|
|
2342
|
+
done
|
|
2343
|
+
if [[ "$FIELDS_CHANGED" == "true" ]]; then
|
|
2344
|
+
log_pass "Flow fields reconciled to: $SELECTED_FIELDS"
|
|
2345
|
+
else
|
|
2346
|
+
log_info "Flow fields already match selected set: $SELECTED_FIELDS"
|
|
2347
|
+
fi
|
|
2348
|
+
|
|
2349
|
+
# Short-circuit only when nothing needs deploying: already active AND no
|
|
2350
|
+
# field change. Otherwise fall through to set-Active + redeploy.
|
|
2351
|
+
if [[ "$FLOW_ACTIVE" == "true" && "$FIELDS_CHANGED" == "false" ]]; then
|
|
2352
|
+
log_pass "Flow is ACTIVE (no field changes needed)"
|
|
2353
|
+
else
|
|
2354
|
+
sed_inplace 's|<status>Draft</status>|<status>Active</status>|g' "$FLOW_FILE"
|
|
2355
|
+
sed_inplace 's|<status>Obsolete</status>|<status>Active</status>|g' "$FLOW_FILE"
|
|
2356
|
+
|
|
2357
|
+
# Reconcile the flow's SalesManagementAgentTemplate constant with the
|
|
2358
|
+
# on-org bot. The retrieved flow carries whatever template was wired
|
|
2359
|
+
# in at last deploy; if the org has since been upgraded (legacy ->
|
|
2360
|
+
# NGA) the constant is stale and the BotDefinition lookup misses.
|
|
2361
|
+
# The second sed handles the symmetric NGA -> legacy downgrade case.
|
|
2362
|
+
#
|
|
2363
|
+
# In this reconcile path we warn-and-skip rather than exit on failure:
|
|
2364
|
+
# the flow already exists on the org, so the worst case is that we
|
|
2365
|
+
# leave the existing constant in place and the deploy below may fail
|
|
2366
|
+
# loudly with a Salesforce-specific error — preferable to aborting
|
|
2367
|
+
# the whole setup script just because we couldn't sync the constant.
|
|
2368
|
+
set +e # Allow pm_bot_agent_template to return non-zero without aborting
|
|
2369
|
+
DETECTED_TEMPLATE=$(pm_bot_agent_template "$ORG_ALIAS")
|
|
2370
|
+
DETECT_RC=$?
|
|
2371
|
+
set -e
|
|
2372
|
+
case $DETECT_RC in
|
|
2373
|
+
0)
|
|
2374
|
+
PM_BOT_TOTAL=$(pm_bot_count "$ORG_ALIAS")
|
|
2375
|
+
if [[ "${PM_BOT_TOTAL:-0}" -gt 1 ]]; then
|
|
2376
|
+
log_warn "Multiple active Pipeline Management bots detected on org (${PM_BOT_TOTAL}). Flow will bind to: $DETECTED_TEMPLATE (NGA preferred). Clean up stale bots to avoid runtime ambiguity."
|
|
2377
|
+
fi
|
|
2378
|
+
sed_inplace "s|<stringValue>SalesMgmt__SalesAgent</stringValue>|<stringValue>${DETECTED_TEMPLATE}</stringValue>|g" "$FLOW_FILE"
|
|
2379
|
+
sed_inplace "s|<stringValue>SalesMgmt__NGASalesAgent</stringValue>|<stringValue>${DETECTED_TEMPLATE}</stringValue>|g" "$FLOW_FILE"
|
|
2380
|
+
log_info "Flow template constant reconciled to: $DETECTED_TEMPLATE"
|
|
2381
|
+
;;
|
|
2382
|
+
1)
|
|
2383
|
+
log_warn "No active Pipeline Management bot found on org. Skipping template-constant reconciliation; activation will proceed with the existing constant and may fail if the bot is missing or inactive."
|
|
2384
|
+
;;
|
|
2385
|
+
*)
|
|
2386
|
+
log_warn "Could not query BotDefinition for AgentTemplate (sf data query failed). Skipping template-constant reconciliation; activation will proceed with the existing constant."
|
|
2387
|
+
;;
|
|
2388
|
+
esac
|
|
2389
|
+
|
|
2390
|
+
if sf project deploy start --metadata "Flow:${FLOW_NAME}" --target-org "$ORG_ALIAS" --json 2>/dev/null | jq -e '.status == 0' >/dev/null 2>&1; then
|
|
2391
|
+
log_pass "Flow activated"
|
|
2392
|
+
else
|
|
2393
|
+
log_warn "Flow activation failed. Activate manually: Setup → Flows → ${FLOW_NAME} → Activate"
|
|
2394
|
+
fi
|
|
2395
|
+
fi # end: active+unchanged short-circuit vs set-Active+redeploy
|
|
2396
|
+
else
|
|
2397
|
+
log_warn "Could not retrieve flow. Activate manually in Setup → Flows."
|
|
2398
|
+
fi
|
|
2399
|
+
fi
|
|
2400
|
+
|
|
2401
|
+
# ============================================================
|
|
2402
|
+
# Phase 6: Stage Descriptions — RELOCATED
|
|
2403
|
+
# ============================================================
|
|
2404
|
+
# Stage-description creation now runs earlier, in Block A as Phase 4c.5 (before the
|
|
2405
|
+
# StageName prompt is deployed/activated/tested), via run_stage_descriptions() in
|
|
2406
|
+
# shared/stage-descriptions.sh. It is a prerequisite for the managed StageName template
|
|
2407
|
+
# to generate anything, so it must precede Phase 4d/4e. Nothing to do here.
|
|
2408
|
+
|
|
2409
|
+
# ============================================================
|
|
2410
|
+
# Phase 7: User Permissions
|
|
2411
|
+
# ============================================================
|
|
2412
|
+
section "Phase 7: User Permissions"
|
|
2413
|
+
|
|
2414
|
+
CURRENT_USER=$(sf org display --target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.username // empty' 2>/dev/null || echo "")
|
|
2415
|
+
CURRENT_USER=$(sanitize_display "$CURRENT_USER" 80)
|
|
2416
|
+
# Sanitize for SOQL interpolation (prevent injection)
|
|
2417
|
+
CURRENT_USER_SAFE=$(sanitize_soql_value "$CURRENT_USER")
|
|
2418
|
+
|
|
2419
|
+
PSG_ID=$(sf data query -q "SELECT Id FROM PermissionSetGroup WHERE DeveloperName = 'SalesManagementUserPsg'" --target-org "$ORG_ALIAS" --use-tooling-api --json 2>/dev/null | jq -r '.result.records[0].Id // empty' 2>/dev/null || echo "")
|
|
2420
|
+
|
|
2421
|
+
if [[ -z "$PSG_ID" ]]; then
|
|
2422
|
+
log_warn "SalesManagementUserPsg not found (provisioning may still be in progress)"
|
|
2423
|
+
else
|
|
2424
|
+
# The PSG recalc is triggered by provisioning and converges asynchronously; we do
|
|
2425
|
+
# NOT block on it. Report its current Status for context, then attempt assignment.
|
|
2426
|
+
# #5: if the PSG has not finished recalculating, the insert is rejected with
|
|
2427
|
+
# INVALID_CROSS_REFERENCE_KEY — assign_user_psg() classifies that explicitly and
|
|
2428
|
+
# tells the operator to re-run, instead of the old `|| log_pass` that masked the
|
|
2429
|
+
# rejection as a bogus "Already assigned".
|
|
2430
|
+
PSG_STATE=$(psg_status "$ORG_ALIAS" "SalesManagementUserPsg")
|
|
2431
|
+
if [[ "$PSG_STATE" != "Updated" ]]; then
|
|
2432
|
+
log_info "SalesManagementUserPsg Status: $PSG_STATE — recalculation may still be in progress; assignment may need a re-run."
|
|
2433
|
+
fi
|
|
2434
|
+
|
|
2435
|
+
# assign_user_psg <user-id> <display-name> — insert PermissionSetAssignment and
|
|
2436
|
+
# classify the outcome. Never masks a real rejection as success.
|
|
2437
|
+
assign_user_psg() {
|
|
2438
|
+
local user_id="$1" display="$2" res msg
|
|
2439
|
+
res=$(sf data create record --sobject PermissionSetAssignment \
|
|
2440
|
+
--values "AssigneeId='${user_id}' PermissionSetGroupId='${PSG_ID}'" \
|
|
2441
|
+
--target-org "$ORG_ALIAS" --json 2>/dev/null || echo '{"status":1}')
|
|
2442
|
+
if echo "$res" | jq -e '.result.id // empty' >/dev/null 2>&1; then
|
|
2443
|
+
log_pass "Assigned SalesManagementUserPsg to: $display"; return 0
|
|
2444
|
+
fi
|
|
2445
|
+
msg=$(echo "$res" | jq -r '.message // .result.message // ""' 2>/dev/null || echo "")
|
|
2446
|
+
if echo "$msg" | grep -qi "DUPLICATE_VALUE"; then
|
|
2447
|
+
log_info "Already assigned: $display"
|
|
2448
|
+
elif echo "$msg" | grep -qi "INVALID_CROSS_REFERENCE_KEY"; then
|
|
2449
|
+
log_warn "Assignment for $display rejected (INVALID_CROSS_REFERENCE_KEY) — SalesManagementUserPsg has not finished recalculating."
|
|
2450
|
+
echo " Re-run: bash setup-all.sh $ORG_ALIAS once the PSG Status reads 'Updated'."
|
|
2451
|
+
track_issue "🟡 WARNING" "User PSG assignment deferred (PSG recalculating)" "Re-run setup-all.sh once SalesManagementUserPsg Status is 'Updated' to finish assigning $display."
|
|
2452
|
+
else
|
|
2453
|
+
log_warn "Failed to assign $display: ${msg:-unknown}"
|
|
2454
|
+
fi
|
|
2455
|
+
return 1
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
# assign_user_list <comma-separated-usernames> — resolve each username to an Id and
|
|
2459
|
+
# assign the PSG. Only ever processes the EXPLICIT list handed to it; there is no
|
|
2460
|
+
# code path that queries all/standard users and bulk-grants. Adding users is always
|
|
2461
|
+
# deliberate: the running user, an operator-typed interactive list, or --users.
|
|
2462
|
+
assign_user_list() {
|
|
2463
|
+
local raw_list="$1" raw_user username username_safe user_id
|
|
2464
|
+
IFS=',' read -ra _USERS <<< "$raw_list"
|
|
2465
|
+
for raw_user in "${_USERS[@]}"; do
|
|
2466
|
+
username=$(echo "$raw_user" | xargs)
|
|
2467
|
+
[[ -z "$username" ]] && continue
|
|
2468
|
+
username_safe=$(sanitize_soql_value "$username")
|
|
2469
|
+
user_id=$(sf data query -q "SELECT Id FROM User WHERE Username = '${username_safe}'" --target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.records[0].Id // empty' 2>/dev/null || echo "")
|
|
2470
|
+
if [[ -z "$user_id" ]]; then
|
|
2471
|
+
log_fail "User not found: $username"
|
|
2472
|
+
else
|
|
2473
|
+
assign_user_psg "$user_id" "$username" || true
|
|
2474
|
+
fi
|
|
2475
|
+
done
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
# Assign current user
|
|
2479
|
+
if [[ -n "$CURRENT_USER" ]]; then
|
|
2480
|
+
USER_ID=$(sf data query -q "SELECT Id FROM User WHERE Username = '${CURRENT_USER_SAFE}'" --target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.records[0].Id // empty' 2>/dev/null || echo "")
|
|
2481
|
+
if [[ -n "$USER_ID" ]]; then
|
|
2482
|
+
assign_user_psg "$USER_ID" "$CURRENT_USER" || true
|
|
2483
|
+
fi
|
|
2484
|
+
fi
|
|
2485
|
+
|
|
2486
|
+
# Explicit --users list (works in both interactive and non-interactive runs).
|
|
2487
|
+
if [[ -n "$USERS_LIST" ]]; then
|
|
2488
|
+
log_info "Assigning SalesManagementUserPsg to --users: $USERS_LIST"
|
|
2489
|
+
assign_user_list "$USERS_LIST"
|
|
2490
|
+
fi
|
|
2491
|
+
|
|
2492
|
+
# Interactive: ask for more users. Non-interactive runs NEVER prompt and NEVER
|
|
2493
|
+
# bulk-assign — the only additional grantees come from the explicit --users flag.
|
|
2494
|
+
if [[ "$NON_INTERACTIVE" != "true" ]]; then
|
|
2495
|
+
echo ""
|
|
2496
|
+
echo " Assign SalesManagementUserPsg to additional users?"
|
|
2497
|
+
read -rp " Usernames (comma-separated, or Enter to skip): " EXTRA_USERS
|
|
2498
|
+
|
|
2499
|
+
if [[ -n "$EXTRA_USERS" ]]; then
|
|
2500
|
+
assign_user_list "$EXTRA_USERS"
|
|
2501
|
+
fi
|
|
2502
|
+
fi
|
|
2503
|
+
fi
|
|
2504
|
+
|
|
2505
|
+
# ============================================================
|
|
2506
|
+
# Phase 8: Final Verification
|
|
2507
|
+
# ============================================================
|
|
2508
|
+
section "Phase 8: Final Verification"
|
|
2509
|
+
|
|
2510
|
+
if [[ -f "$SCRIPT_DIR/verify-all.sh" ]]; then
|
|
2511
|
+
# verify-all.sh is a DIAGNOSTIC: it exits nonzero when it reports any FAIL
|
|
2512
|
+
# (e.g. stage descriptions absent on a NextStep-only run). That is informational
|
|
2513
|
+
# status, NOT a setup failure — swallow it with `|| true` so `set -e` does not
|
|
2514
|
+
# abort here and skip the Configuration Summary table below. Capture the exit so
|
|
2515
|
+
# the summary can note that verification flagged issues to review.
|
|
2516
|
+
VERIFY_EXIT=0
|
|
2517
|
+
bash "$SCRIPT_DIR/verify-all.sh" "$ORG_ALIAS" || VERIFY_EXIT=$?
|
|
2518
|
+
if [[ "$VERIFY_EXIT" -ne 0 ]]; then
|
|
2519
|
+
log_warn "verify-all.sh reported issues (exit $VERIFY_EXIT) — review its output above; setup summary follows."
|
|
2520
|
+
fi
|
|
2521
|
+
else
|
|
2522
|
+
log_warn "verify-all.sh not found. Run manually to confirm setup."
|
|
2523
|
+
fi
|
|
2524
|
+
|
|
2525
|
+
# ============================================================
|
|
2526
|
+
# Phase 8.5: Stage Descriptions Summary Table
|
|
2527
|
+
# ============================================================
|
|
2528
|
+
# Display all persisted OpptStageDescription records after setup
|
|
2529
|
+
|
|
2530
|
+
if [[ "${STAGE_DESCRIPTIONS_BLOCKED:-}" != "true" ]]; then
|
|
2531
|
+
# Query all stage descriptions (global-per-stage; no OpportunityRecordTypeId column)
|
|
2532
|
+
SUMMARY_QUERY=$(sf data query --query "SELECT Id, MasterLabel, OpportunityStageApiName, Description FROM OpptStageDescription ORDER BY MasterLabel" --target-org "$ORG_ALIAS" --use-tooling-api --json 2>/dev/null || echo '{"status":1}')
|
|
2533
|
+
|
|
2534
|
+
SUMMARY_COUNT=$(echo "$SUMMARY_QUERY" | jq -r '.result.totalSize // 0' 2>/dev/null | head -1 | tr -cd '0-9' || echo "0")
|
|
2535
|
+
SUMMARY_COUNT="${SUMMARY_COUNT:-0}"
|
|
2536
|
+
|
|
2537
|
+
if [[ "$SUMMARY_COUNT" -gt 0 ]]; then
|
|
2538
|
+
echo ""
|
|
2539
|
+
section "Stage Descriptions Summary"
|
|
2540
|
+
echo ""
|
|
2541
|
+
echo "Persisted OpptStageDescription records: $SUMMARY_COUNT"
|
|
2542
|
+
echo ""
|
|
2543
|
+
|
|
2544
|
+
# Determine methodology from descriptions (detect MEDDIC/BANT/SPICED keywords)
|
|
2545
|
+
FIRST_DESC=$(echo "$SUMMARY_QUERY" | jq -r '.result.records[0].Description // ""' 2>/dev/null)
|
|
2546
|
+
METHODOLOGY="Unknown"
|
|
2547
|
+
if echo "$FIRST_DESC" | grep -qi "MEDDIC\|Metrics.*Economic Buyer"; then
|
|
2548
|
+
METHODOLOGY="MEDDIC"
|
|
2549
|
+
elif echo "$FIRST_DESC" | grep -qi "BANT\|Budget.*Authority"; then
|
|
2550
|
+
METHODOLOGY="BANT"
|
|
2551
|
+
elif echo "$FIRST_DESC" | grep -qi "SPICED\|Situation.*Pain.*Impact"; then
|
|
2552
|
+
METHODOLOGY="SPICED"
|
|
2553
|
+
fi
|
|
2554
|
+
|
|
2555
|
+
# Print table header — descriptions are global-per-stage, no record-type column
|
|
2556
|
+
printf "┌──────────────────────────┬──────────────────────────────────────────────────────────────┬────────────┐\n"
|
|
2557
|
+
printf "│ %-24s │ %-60s │ %-10s │\n" "Stage Name" "Description (first 60 chars)" "Methodology"
|
|
2558
|
+
printf "├──────────────────────────┼──────────────────────────────────────────────────────────────┼────────────┤\n"
|
|
2559
|
+
|
|
2560
|
+
# Print table rows
|
|
2561
|
+
while IFS= read -r DESC_JSON; do
|
|
2562
|
+
[[ -z "$DESC_JSON" ]] && continue
|
|
2563
|
+
|
|
2564
|
+
STAGE_NAME=$(echo "$DESC_JSON" | jq -r '.MasterLabel // .OpportunityStageApiName // "N/A"' 2>/dev/null)
|
|
2565
|
+
STAGE_DESC=$(echo "$DESC_JSON" | jq -r '.Description // "N/A"' 2>/dev/null)
|
|
2566
|
+
|
|
2567
|
+
# Truncate description to 60 characters
|
|
2568
|
+
if [[ ${#STAGE_DESC} -gt 60 ]]; then
|
|
2569
|
+
STAGE_DESC_TRUNC="${STAGE_DESC:0:57}..."
|
|
2570
|
+
else
|
|
2571
|
+
STAGE_DESC_TRUNC="$STAGE_DESC"
|
|
2572
|
+
fi
|
|
2573
|
+
|
|
2574
|
+
# Print row
|
|
2575
|
+
printf "│ %-24s │ %-60s │ %-10s │\n" "$STAGE_NAME" "$STAGE_DESC_TRUNC" "$METHODOLOGY"
|
|
2576
|
+
|
|
2577
|
+
done < <(echo "$SUMMARY_QUERY" | jq -c '.result.records[]?' 2>/dev/null)
|
|
2578
|
+
|
|
2579
|
+
# Print table footer
|
|
2580
|
+
printf "└──────────────────────────┴──────────────────────────────────────────────────────────────┴────────────┘\n"
|
|
2581
|
+
echo ""
|
|
2582
|
+
echo "Note: Full descriptions are stored in Salesforce. This table shows first 60 characters only."
|
|
2583
|
+
echo ""
|
|
2584
|
+
fi
|
|
2585
|
+
fi
|
|
2586
|
+
|
|
2587
|
+
echo ""
|
|
2588
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
2589
|
+
echo " PIPELINE MANAGEMENT SETUP - CONFIGURATION SUMMARY "
|
|
2590
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
2591
|
+
echo ""
|
|
2592
|
+
|
|
2593
|
+
# Query current org configuration state
|
|
2594
|
+
log_info "Querying final org configuration state..."
|
|
2595
|
+
echo ""
|
|
2596
|
+
|
|
2597
|
+
# Build configuration summary
|
|
2598
|
+
printf "┌──────────────────────────────────────────┬─────────────────────┬─────────────────────┬────────────────┐\n"
|
|
2599
|
+
printf "│ %-40s │ %-19s │ %-19s │ %-14s │\n" "Configuration Item" "Before Setup" "After Setup" "Status"
|
|
2600
|
+
printf "├──────────────────────────────────────────┼─────────────────────┼─────────────────────┼────────────────┤\n"
|
|
2601
|
+
|
|
2602
|
+
# Configuration Summary rows compare BEFORE_* (snapshot captured pre-Phase-3)
|
|
2603
|
+
# vs. AFTER_* (live post-run query). Emit UNCHANGED when they match so
|
|
2604
|
+
# idempotent reruns don't misreport as CHANGED. See W-23277501.
|
|
2605
|
+
|
|
2606
|
+
# Cache one SOAP read so both settings come from the same document
|
|
2607
|
+
SOAP_SETTINGS_AFTER=$(soap_read "SalesDealAgentSettings" "SalesDealAgent" 2>/dev/null || echo "")
|
|
2608
|
+
|
|
2609
|
+
# --- Pipeline Management ---
|
|
2610
|
+
AFTER_PM=$(soap_extract_bool "$SOAP_SETTINGS_AFTER" "enableDealAgent" 2>/dev/null || echo "false")
|
|
2611
|
+
PM_BEFORE_LABEL=$([ "$BEFORE_PM" == "true" ] && echo "✅ Enabled" || echo "❌ Not Enabled")
|
|
2612
|
+
PM_AFTER_LABEL=$([ "$AFTER_PM" == "true" ] && echo "✅ Enabled" || echo "❌ Not Enabled")
|
|
2613
|
+
if [[ "$BEFORE_PM" == "$AFTER_PM" ]]; then
|
|
2614
|
+
PM_STATUS_COL=$([ "$AFTER_PM" == "true" ] && echo "✅ UNCHANGED" || echo "⚠️ NOT ENABLED")
|
|
2615
|
+
else
|
|
2616
|
+
PM_STATUS_COL=$([ "$AFTER_PM" == "true" ] && echo "✅ CHANGED" || echo "⚠️ REVERTED")
|
|
2617
|
+
fi
|
|
2618
|
+
printf "│ %-40s │ %-19s │ %-19s │ %-14s │\n" "Pipeline Management" "$PM_BEFORE_LABEL" "$PM_AFTER_LABEL" "$PM_STATUS_COL"
|
|
2619
|
+
|
|
2620
|
+
# --- Autonomous Field Updates ---
|
|
2621
|
+
AFTER_AUTO=$(soap_extract_bool "$SOAP_SETTINGS_AFTER" "enableDealAgentAutoApproveAllTasks" 2>/dev/null || echo "false")
|
|
2622
|
+
AUTO_BEFORE_LABEL=$([ "$BEFORE_AUTO" == "true" ] && echo "✅ Enabled" || echo "❌ Disabled")
|
|
2623
|
+
AUTO_AFTER_LABEL=$([ "$AFTER_AUTO" == "true" ] && echo "✅ Enabled" || echo "❌ Disabled")
|
|
2624
|
+
if [[ "$BEFORE_AUTO" == "$AFTER_AUTO" ]]; then
|
|
2625
|
+
AUTO_STATUS_COL=$([ "$AFTER_AUTO" == "true" ] && echo "✅ UNCHANGED" || echo "⚠️ NOT ENABLED")
|
|
2626
|
+
else
|
|
2627
|
+
AUTO_STATUS_COL=$([ "$AFTER_AUTO" == "true" ] && echo "✅ CHANGED" || echo "⚠️ REVERTED")
|
|
2628
|
+
fi
|
|
2629
|
+
printf "│ %-40s │ %-19s │ %-19s │ %-14s │\n" "Autonomous Field Updates" "$AUTO_BEFORE_LABEL" "$AUTO_AFTER_LABEL" "$AUTO_STATUS_COL"
|
|
2630
|
+
|
|
2631
|
+
# --- Agent User ---
|
|
2632
|
+
AFTER_AGENT_USER=$(sf data query --query "SELECT Username FROM User WHERE Username LIKE '%salesmanagementagentuser%'" --target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.records[0].Username // "Not Created"' 2>/dev/null || echo "Not Created")
|
|
2633
|
+
AFTER_AGENT_USER=$(sanitize_display "$AFTER_AGENT_USER" 60)
|
|
2634
|
+
AGENT_BEFORE_LABEL=$([ "$BEFORE_AGENT_USER" != "Not Created" ] && echo "✅ Created" || echo "Not Created")
|
|
2635
|
+
AGENT_AFTER_LABEL=$([ "$AFTER_AGENT_USER" != "Not Created" ] && echo "✅ Created" || echo "❌ Not Created")
|
|
2636
|
+
if [[ "$BEFORE_AGENT_USER" == "$AFTER_AGENT_USER" ]]; then
|
|
2637
|
+
AGENT_STATUS_COL=$([ "$AFTER_AGENT_USER" != "Not Created" ] && echo "✅ UNCHANGED" || echo "⚠️ NOT CREATED")
|
|
2638
|
+
else
|
|
2639
|
+
AGENT_STATUS_COL=$([ "$AFTER_AGENT_USER" != "Not Created" ] && echo "✅ CREATED" || echo "⚠️ REMOVED")
|
|
2640
|
+
fi
|
|
2641
|
+
printf "│ %-40s │ %-19s │ %-19s │ %-14s │\n" "Agent User (salesmanagementagent...)" "$AGENT_BEFORE_LABEL" "$AGENT_AFTER_LABEL" "$AGENT_STATUS_COL"
|
|
2642
|
+
|
|
2643
|
+
# --- PSGs ---
|
|
2644
|
+
AFTER_PSG_COUNT=$(sf data query --query "SELECT Id FROM PermissionSetGroup WHERE DeveloperName IN ('SalesManagementUserPsg','SalesManagementAgentUserPsg')" --target-org "$ORG_ALIAS" --use-tooling-api --json 2>/dev/null | jq -r '.result.totalSize // 0' 2>/dev/null || echo "0")
|
|
2645
|
+
AFTER_PSG_COUNT=$((${AFTER_PSG_COUNT:-0} + 0))
|
|
2646
|
+
if [[ "$BEFORE_PSG_COUNT" == "$AFTER_PSG_COUNT" ]]; then
|
|
2647
|
+
PSG_STATUS_COL=$([ "$AFTER_PSG_COUNT" -ge 2 ] && echo "✅ UNCHANGED" || echo "⚠️ PARTIAL")
|
|
2648
|
+
else
|
|
2649
|
+
PSG_STATUS_COL=$([ "$AFTER_PSG_COUNT" -ge 2 ] && echo "✅ CREATED" || echo "⚠️ PARTIAL")
|
|
2650
|
+
fi
|
|
2651
|
+
printf "│ %-40s │ %-19s │ %-19s │ %-14s │\n" "Permission Set Groups (license)" "$BEFORE_PSG_COUNT PSGs" "$AFTER_PSG_COUNT PSGs" "$PSG_STATUS_COL"
|
|
2652
|
+
|
|
2653
|
+
# --- Schedule Flow ---
|
|
2654
|
+
# Resolve via the shared SourceTemplateId-first resolver (#6) so a freshly deployed
|
|
2655
|
+
# flow is not misreported as "Not Found" by a fuzzy label query.
|
|
2656
|
+
AFTER_FLOW_ACTIVE=$(schedule_flow_active "$ORG_ALIAS")
|
|
2657
|
+
case "$BEFORE_FLOW_ACTIVE" in
|
|
2658
|
+
true) FLOW_BEFORE_LABEL="✅ Active" ;;
|
|
2659
|
+
false) FLOW_BEFORE_LABEL="⚠️ Draft" ;;
|
|
2660
|
+
*) FLOW_BEFORE_LABEL="Not Deployed" ;;
|
|
2661
|
+
esac
|
|
2662
|
+
case "$AFTER_FLOW_ACTIVE" in
|
|
2663
|
+
true) FLOW_AFTER_LABEL="✅ Active" ;;
|
|
2664
|
+
false) FLOW_AFTER_LABEL="⚠️ Draft" ;;
|
|
2665
|
+
*) FLOW_AFTER_LABEL="❌ Not Found" ;;
|
|
2666
|
+
esac
|
|
2667
|
+
if [[ "$BEFORE_FLOW_ACTIVE" == "$AFTER_FLOW_ACTIVE" ]]; then
|
|
2668
|
+
FLOW_STATUS_COL=$([ "$AFTER_FLOW_ACTIVE" == "true" ] && echo "✅ UNCHANGED" || echo "⚠️ PARTIAL")
|
|
2669
|
+
else
|
|
2670
|
+
FLOW_STATUS_COL=$([ "$AFTER_FLOW_ACTIVE" == "true" ] && echo "✅ CHANGED" || echo "⚠️ PARTIAL")
|
|
2671
|
+
fi
|
|
2672
|
+
printf "│ %-40s │ %-19s │ %-19s │ %-14s │\n" "Schedule Flow (Suggestions)" "$FLOW_BEFORE_LABEL" "$FLOW_AFTER_LABEL" "$FLOW_STATUS_COL"
|
|
2673
|
+
|
|
2674
|
+
# --- Stage Descriptions ---
|
|
2675
|
+
# Same INVALID_TYPE / "0\n0" arithmetic-crash guard as the BEFORE snapshot above:
|
|
2676
|
+
# if OpptStageDescription is not yet queryable (PM enable still settling), `|| true`
|
|
2677
|
+
# lets jq's `// 0` default stand instead of appending a second line.
|
|
2678
|
+
AFTER_STAGE_DESC_COUNT=$(sf data query --query "SELECT Id FROM OpptStageDescription" --target-org "$ORG_ALIAS" --use-tooling-api --json 2>/dev/null | jq -r '.result.totalSize // 0' 2>/dev/null || true)
|
|
2679
|
+
AFTER_STAGE_DESC_COUNT=$((${AFTER_STAGE_DESC_COUNT:-0} + 0))
|
|
2680
|
+
if [[ "$BEFORE_STAGE_DESC_COUNT" == "$AFTER_STAGE_DESC_COUNT" ]]; then
|
|
2681
|
+
DESC_STATUS_COL=$([ "$AFTER_STAGE_DESC_COUNT" -gt 0 ] && echo "✅ UNCHANGED" || echo "⚠️ NONE")
|
|
2682
|
+
else
|
|
2683
|
+
DESC_STATUS_COL=$([ "$AFTER_STAGE_DESC_COUNT" -gt 0 ] && echo "✅ CREATED" || echo "⚠️ NONE")
|
|
2684
|
+
fi
|
|
2685
|
+
printf "│ %-40s │ %-19s │ %-19s │ %-14s │\n" "Opportunity Stage Descriptions" "$BEFORE_STAGE_DESC_COUNT descriptions" "$AFTER_STAGE_DESC_COUNT descriptions" "$DESC_STATUS_COL"
|
|
2686
|
+
|
|
2687
|
+
printf "└──────────────────────────────────────────┴─────────────────────┴─────────────────────┴────────────────┘\n"
|
|
2688
|
+
echo ""
|
|
2689
|
+
|
|
2690
|
+
# Feature Dependency Sequence
|
|
2691
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
2692
|
+
echo " FEATURE DEPENDENCY SEQUENCE "
|
|
2693
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
2694
|
+
echo ""
|
|
2695
|
+
echo " Phase 1: Prerequisites"
|
|
2696
|
+
echo " ├─ SOAP API Enabled"
|
|
2697
|
+
echo " ├─ User: 'Use Any API Auth' Permission"
|
|
2698
|
+
echo " ├─ Einstein GenAI Enabled"
|
|
2699
|
+
echo " └─ Enhanced Notes Enabled"
|
|
2700
|
+
echo " │"
|
|
2701
|
+
echo " ▼"
|
|
2702
|
+
echo " Phase 2: Prerequisite Objects"
|
|
2703
|
+
echo " ├─ Enhanced Notes Configuration"
|
|
2704
|
+
echo " └─ Opportunity Splits Configuration"
|
|
2705
|
+
echo " │"
|
|
2706
|
+
echo " ▼"
|
|
2707
|
+
echo " Phase 3: Pipeline Management Enablement"
|
|
2708
|
+
echo " └─ SalesDealAgentSettings.enableDealAgent = true"
|
|
2709
|
+
echo " │"
|
|
2710
|
+
echo " ▼"
|
|
2711
|
+
echo " Phase 3.5: Autonomous Field Updates (opt-in via --autonomous)"
|
|
2712
|
+
echo " ├─ Requires: Phase 3 Complete"
|
|
2713
|
+
echo " └─ Sets: enableDealAgentAutoApproveAllTasks = true (only when opted in; else suggestions-only)"
|
|
2714
|
+
echo " │"
|
|
2715
|
+
echo " ▼"
|
|
2716
|
+
echo " Phase 4: Provisioning (async)"
|
|
2717
|
+
echo " ├─ Agent User: salesmanagementagentuser@..."
|
|
2718
|
+
echo " └─ PSGs: SalesManagementUserPsg, SalesManagementAgentUserPsg"
|
|
2719
|
+
echo " │"
|
|
2720
|
+
echo " ▼"
|
|
2721
|
+
echo " Phase 4c.5: Opportunity Stage Descriptions (only if StageName selected; opt-in via --create-stage-descriptions)"
|
|
2722
|
+
echo " ├─ Requires: Phase 4 Complete (OpptStageDescription API accessible)"
|
|
2723
|
+
echo " ├─ Runs BEFORE the StageName prompt is deployed/tested (it is the grounding)"
|
|
2724
|
+
echo " ├─ Standard Stages: Methodology-specific descriptions"
|
|
2725
|
+
echo " └─ Custom Stages: Context-aware recommendations (user confirmation)"
|
|
2726
|
+
echo " │"
|
|
2727
|
+
echo " ▼"
|
|
2728
|
+
echo " Phase 5: Schedule-Triggered Flow"
|
|
2729
|
+
echo " └─ Deploy & Activate: Process_Field_Update_Suggestions"
|
|
2730
|
+
echo " │"
|
|
2731
|
+
echo " ▼"
|
|
2732
|
+
echo " Phase 7: User Permissions"
|
|
2733
|
+
echo " └─ Assign: SalesManagementUserPsg to users"
|
|
2734
|
+
echo ""
|
|
2735
|
+
|
|
2736
|
+
# Issues Encountered
|
|
2737
|
+
if [[ ${#SETUP_ISSUES[@]} -gt 0 ]]; then
|
|
2738
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
2739
|
+
echo " ISSUES ENCOUNTERED "
|
|
2740
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
2741
|
+
echo ""
|
|
2742
|
+
printf "┌──────────┬────────────────────────────────────────────────────────────┬──────────────────────────────────┐\n"
|
|
2743
|
+
printf "│ %-8s │ %-58s │ %-32s │\n" "Severity" "Issue Description" "Resolution"
|
|
2744
|
+
printf "├──────────┼────────────────────────────────────────────────────────────┼──────────────────────────────────┤\n"
|
|
2745
|
+
|
|
2746
|
+
for ISSUE in "${SETUP_ISSUES[@]}"; do
|
|
2747
|
+
IFS='|' read -r SEVERITY DESC RESOLUTION <<< "$ISSUE"
|
|
2748
|
+
printf "│ %-8s │ %-58s │ %-32s │\n" "$SEVERITY" "${DESC:0:58}" "${RESOLUTION:0:32}"
|
|
2749
|
+
done
|
|
2750
|
+
|
|
2751
|
+
printf "└──────────┴────────────────────────────────────────────────────────────┴──────────────────────────────────┘\n"
|
|
2752
|
+
echo ""
|
|
2753
|
+
fi
|
|
2754
|
+
|
|
2755
|
+
# A tracked "🔴 BLOCKER" means setup did NOT complete — do not report success.
|
|
2756
|
+
# (track_issue only records; some blocking paths exit inline, but a BLOCKER can also
|
|
2757
|
+
# be recorded without an inline exit, e.g. bot-activation failure at Phase 4b. This
|
|
2758
|
+
# gate ensures such a run reports SETUP INCOMPLETE and exits non-zero.)
|
|
2759
|
+
# NOTE: only "🔴 BLOCKER" is terminal. "🔴 ERROR" is provisional — it marks a state
|
|
2760
|
+
# the script then attempts to recover from (e.g. SOAP re-provisioning toggle); a
|
|
2761
|
+
# recovered run must still be allowed to report SETUP COMPLETE.
|
|
2762
|
+
BLOCKER_COUNT=0
|
|
2763
|
+
for ISSUE in "${SETUP_ISSUES[@]}"; do
|
|
2764
|
+
case "$ISSUE" in
|
|
2765
|
+
"🔴 BLOCKER"*) BLOCKER_COUNT=$((BLOCKER_COUNT + 1)) ;;
|
|
2766
|
+
esac
|
|
2767
|
+
done
|
|
2768
|
+
|
|
2769
|
+
if [[ "$BLOCKER_COUNT" -gt 0 ]]; then
|
|
2770
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
2771
|
+
echo " SETUP INCOMPLETE "
|
|
2772
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
2773
|
+
echo ""
|
|
2774
|
+
echo " $BLOCKER_COUNT blocking issue(s) (🔴) were recorded above. Setup did not finish successfully."
|
|
2775
|
+
echo " Resolve the issue(s) in the table and re-run: bash setup-all.sh $ORG_ALIAS"
|
|
2776
|
+
echo ""
|
|
2777
|
+
exit 1
|
|
2778
|
+
fi
|
|
2779
|
+
|
|
2780
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
2781
|
+
echo " SETUP COMPLETE "
|
|
2782
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
2783
|
+
echo ""
|
|
2784
|
+
echo "Next steps:"
|
|
2785
|
+
echo " - If stage descriptions show FAIL, re-run in 5 minutes (provisioning)"
|
|
2786
|
+
echo " - Add notes/emails to opportunities to give the agent data"
|
|
2787
|
+
echo " - Wait for daily schedule flow (or check Setup → Flows → run history)"
|
|
2788
|
+
echo " - Agent Analytics: Setup → Einstein Feedback and Monitoring"
|
|
2789
|
+
echo ""
|
|
2790
|
+
echo "Setup URL: $(sanitize_display "${INSTANCE_URL}/lightning/setup/PipelineManagement/home" 80)"
|
|
2791
|
+
echo ""
|