@salesforce/afv-skills 1.30.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/data360-code-extension-generate/SKILL.md +15 -2
- 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/dx-pkg-post-install-configure/SKILL.md +184 -0
- package/skills/experience-ui-bundle-app-coordinate/SKILL.md +202 -35
- package/skills/experience-ui-bundle-app-coordinate/scripts/check-hosting-target.sh +26 -0
- package/skills/experience-ui-bundle-app-coordinate/scripts/check-sfdx-project.sh +15 -0
- 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-metadata-generate/SKILL.md +19 -8
- package/skills/experience-ui-bundle-metadata-generate/references/csp-metadata-format.md +2 -2
- package/skills/experience-ui-bundle-metadata-generate/scripts/check-api-version.sh +30 -0
- 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-field-generate/SKILL.md +12 -11
- package/skills/platform-custom-field-generate/references/advanced-picklists.md +17 -17
- 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-custom-report-type-generate/SKILL.md +259 -0
- package/skills/platform-custom-report-type-generate/examples/AccountProjectsWithTasks.reportType-meta.xml +51 -0
- package/skills/platform-custom-report-type-generate/examples/AccountsWithIndustry.reportType-meta.xml +27 -0
- package/skills/platform-custom-report-type-generate/examples/AccountsWithProjects.reportType-meta.xml +44 -0
- package/skills/platform-custom-report-type-generate/references/category-values.md +32 -0
- package/skills/platform-custom-report-type-generate/references/errors-and-troubleshooting.md +24 -0
- package/skills/platform-dataspace-access-configure/SKILL.md +318 -0
- package/skills/platform-docs-get/SKILL.md +4 -0
- package/skills/platform-docs-get/scripts/extract_salesforce_doc.py +78 -13
- package/skills/platform-encryption-configure/SKILL.md +122 -0
- package/skills/platform-encryption-configure/assets/EncryptionKey.settings-meta.xml +17 -0
- package/skills/platform-encryption-configure/assets/PlatformEncryption.settings-meta.xml +15 -0
- package/skills/platform-encryption-configure/assets/encrypted-field.field-meta.xml +22 -0
- package/skills/platform-encryption-configure/examples/cache-only-keys.settings-meta.xml +11 -0
- package/skills/platform-encryption-configure/references/encryption-schemes.md +34 -0
- package/skills/platform-encryption-configure/references/key-models.md +34 -0
- package/skills/platform-encryption-configure/references/tenant-secret-lifecycle.md +23 -0
- package/skills/platform-encryption-configure/scripts/validate-encryption-metadata.sh +54 -0
- 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-metadata-api-context-get/SKILL.md +13 -10
- 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,48 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# shared/psg.sh — shared PermissionSetGroup recalculation helper.
|
|
3
|
+
#
|
|
4
|
+
# PermissionSetGroup.Status is a system-computed field (Updated / Updating /
|
|
5
|
+
# Outdated / Failed), NOT client-writable. A component change (inserting a
|
|
6
|
+
# PermissionSetGroupComponent) is what TRIGGERS the async recalculation — there
|
|
7
|
+
# is no CLI recalc verb and no client-writable nudge.
|
|
8
|
+
#
|
|
9
|
+
# Design choice (deliberate): setup does NOT block waiting for recalculation to
|
|
10
|
+
# finish. Firing the trigger is sufficient for the org to converge on its own.
|
|
11
|
+
# The recalc's completion only matters later, when a user tries to VIEW
|
|
12
|
+
# suggestions in the UI or the scheduled flow RUNS a generation — both of which
|
|
13
|
+
# happen well after setup exits. So callers only ever read Status once, for
|
|
14
|
+
# reporting, and never sleep-poll.
|
|
15
|
+
#
|
|
16
|
+
# This file only DEFINES a function; it makes no org calls when sourced.
|
|
17
|
+
|
|
18
|
+
# psg_status <org-alias> <psg-developer-name>
|
|
19
|
+
#
|
|
20
|
+
# Reads the named PSG's recalculation Status ONCE (no waiting). Prints one of:
|
|
21
|
+
# Updated | Updating | Outdated | Failed (the raw Status value), or
|
|
22
|
+
# not-found (PSG DeveloperName did not resolve), or
|
|
23
|
+
# unknown (query/parse failed)
|
|
24
|
+
# Always returns 0 — the caller inspects the printed value. Never sleeps.
|
|
25
|
+
psg_status() {
|
|
26
|
+
local org="$1" psg_name="$2"
|
|
27
|
+
local psg_id
|
|
28
|
+
psg_id=$(sf data query \
|
|
29
|
+
--query "SELECT Id FROM PermissionSetGroup WHERE DeveloperName = '${psg_name}'" \
|
|
30
|
+
--target-org "$org" \
|
|
31
|
+
--use-tooling-api \
|
|
32
|
+
--json 2>/dev/null | jq -r '.result.records[0].Id // empty' 2>/dev/null || echo "")
|
|
33
|
+
|
|
34
|
+
if [[ -z "$psg_id" ]]; then
|
|
35
|
+
printf 'not-found'
|
|
36
|
+
return 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
local status
|
|
40
|
+
status=$(sf data query \
|
|
41
|
+
--query "SELECT Status FROM PermissionSetGroup WHERE Id = '${psg_id}'" \
|
|
42
|
+
--target-org "$org" \
|
|
43
|
+
--use-tooling-api \
|
|
44
|
+
--json 2>/dev/null | jq -r '.result.records[0].Status // empty' 2>/dev/null || echo "")
|
|
45
|
+
|
|
46
|
+
printf '%s' "${status:-unknown}"
|
|
47
|
+
return 0
|
|
48
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Shared library for SOAP Metadata API response parsing
|
|
3
|
+
#
|
|
4
|
+
# Functions:
|
|
5
|
+
# redact_token <input> - Redacts session IDs and Bearer tokens from error messages
|
|
6
|
+
# parse_soap_response <xml> <field> <http_status> - Classifies SOAP responses
|
|
7
|
+
#
|
|
8
|
+
# Returns:
|
|
9
|
+
# "true" - field is enabled
|
|
10
|
+
# "false" - field is explicitly disabled or omitted (setting is off)
|
|
11
|
+
# "AUTH_ERROR:<fault>" - authentication/authorization failure
|
|
12
|
+
# "TYPE_UNAVAILABLE:<fault>" - org lacks license/edition for this feature
|
|
13
|
+
# "NETWORK_ERROR" - network/service error (empty response, 5xx, malformed XML)
|
|
14
|
+
|
|
15
|
+
# Redact session IDs and Bearer tokens from error messages
|
|
16
|
+
# Args: $1 = input string
|
|
17
|
+
# Returns: string with tokens replaced by [REDACTED]
|
|
18
|
+
#
|
|
19
|
+
# Salesforce session IDs start with the 15/18-char org id (00D...) followed by
|
|
20
|
+
# '!' and a signature segment that contains '.', '=', '-', '+' in addition to
|
|
21
|
+
# alphanumerics. Match greedily up to the next whitespace or XML tag boundary
|
|
22
|
+
# ('<') so the whole token tail is redacted, not just its leading run.
|
|
23
|
+
redact_token() {
|
|
24
|
+
local input="$1"
|
|
25
|
+
printf '%s' "$input" | sed 's/00D[^ <]*/[REDACTED]/g; s/Bearer [^ ]*/Bearer [REDACTED]/g'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Parse SOAP Metadata API response and classify the result
|
|
29
|
+
# Args:
|
|
30
|
+
# $1 = raw SOAP XML response
|
|
31
|
+
# $2 = field name to extract (e.g., "enableEinsteinGptPlatform")
|
|
32
|
+
# $3 = HTTP status code (e.g., "200", "503")
|
|
33
|
+
# Returns: classification string (see header comments)
|
|
34
|
+
parse_soap_response() {
|
|
35
|
+
local xml_response="$1"
|
|
36
|
+
local field_name="$2"
|
|
37
|
+
local http_status="$3"
|
|
38
|
+
|
|
39
|
+
# Validate field_name to prevent regex injection in grep patterns below
|
|
40
|
+
if [[ ! "$field_name" =~ ^[A-Za-z0-9_]+$ ]]; then
|
|
41
|
+
echo "NETWORK_ERROR"
|
|
42
|
+
return 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# Disable pipefail locally to allow grep failures without aborting
|
|
46
|
+
local old_opts="$-"
|
|
47
|
+
set +e
|
|
48
|
+
|
|
49
|
+
# Check for network/service errors first (non-2xx status or empty response)
|
|
50
|
+
if [[ -z "$xml_response" ]] || [[ "$http_status" != "200" && "$http_status" != "201" ]]; then
|
|
51
|
+
# Restore pipefail before returning
|
|
52
|
+
if [[ "$old_opts" =~ e ]]; then set -e; fi
|
|
53
|
+
echo "NETWORK_ERROR"
|
|
54
|
+
return 0
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# Check for SOAP Fault (auth, permission, or type errors)
|
|
58
|
+
# Handle both standard and namespace-prefixed faults
|
|
59
|
+
local fault_code=""
|
|
60
|
+
if echo "$xml_response" | grep -q "<soapenv:Fault>\\|<sf:Fault>\\|<Fault>"; then
|
|
61
|
+
# Extract faultcode - try multiple patterns for different namespace conventions
|
|
62
|
+
fault_code=$(echo "$xml_response" | grep -o "<faultcode>[^<]*</faultcode>" | sed 's/<[^>]*>//g' | head -1)
|
|
63
|
+
if [[ -z "$fault_code" ]]; then
|
|
64
|
+
fault_code=$(echo "$xml_response" | grep -o "<sf:faultcode>[^<]*</sf:faultcode>" | sed 's/<[^>]*>//g' | head -1)
|
|
65
|
+
fi
|
|
66
|
+
if [[ -z "$fault_code" ]]; then
|
|
67
|
+
# Handle fully-qualified fault codes like "sf:INVALID_SESSION_ID"
|
|
68
|
+
fault_code=$(echo "$xml_response" | grep -oE "faultcode>[^<]*</.*faultcode" | sed 's/<[^>]*>//g' | sed 's/.*://' | head -1)
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
if [[ -n "$fault_code" ]]; then
|
|
72
|
+
# Extract faultstring for context (will be redacted)
|
|
73
|
+
local fault_string=""
|
|
74
|
+
fault_string=$(echo "$xml_response" | grep -o "<faultstring>[^<]*</faultstring>" | sed 's/<[^>]*>//g' | head -1 || echo "")
|
|
75
|
+
if [[ -z "$fault_string" ]]; then
|
|
76
|
+
fault_string=$(echo "$xml_response" | grep -o "<sf:faultstring>[^<]*</sf:faultstring>" | sed 's/<[^>]*>//g' | head -1 || echo "")
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# Redact tokens from fault string
|
|
80
|
+
if [[ -n "$fault_string" ]]; then
|
|
81
|
+
fault_string=$(redact_token "$fault_string")
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# Classify by fault type
|
|
85
|
+
# INVALID_TYPE = org lacks license/edition - distinct from auth failures
|
|
86
|
+
if echo "$fault_code" | grep -qi "INVALID_TYPE"; then
|
|
87
|
+
if [[ "$old_opts" =~ e ]]; then set -e; fi
|
|
88
|
+
echo "TYPE_UNAVAILABLE:${fault_code}${fault_string:+:$fault_string}"
|
|
89
|
+
return 0
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# All other faults are auth/permission errors
|
|
93
|
+
if [[ "$old_opts" =~ e ]]; then set -e; fi
|
|
94
|
+
echo "AUTH_ERROR:${fault_code}${fault_string:+:$fault_string}"
|
|
95
|
+
return 0
|
|
96
|
+
fi
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# Check if response is valid XML with the expected structure
|
|
100
|
+
# If it's HTML or malformed XML, treat as network error
|
|
101
|
+
if ! echo "$xml_response" | grep -q "<readMetadataResponse>\\|<updateMetadataResponse>"; then
|
|
102
|
+
if [[ "$old_opts" =~ e ]]; then set -e; fi
|
|
103
|
+
echo "NETWORK_ERROR"
|
|
104
|
+
return 0
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
# Check for closing envelope tag - if missing, XML is truncated
|
|
108
|
+
if ! echo "$xml_response" | grep -q "</soapenv:Envelope>"; then
|
|
109
|
+
if [[ "$old_opts" =~ e ]]; then set -e; fi
|
|
110
|
+
echo "NETWORK_ERROR"
|
|
111
|
+
return 0
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# Extract field value - try to find <field>true/false</field>
|
|
115
|
+
# head -1 collapses multi-record responses to the first match so two records
|
|
116
|
+
# never produce a two-line value that fails the == comparisons below.
|
|
117
|
+
local field_value=""
|
|
118
|
+
field_value=$(echo "$xml_response" | grep -o "<${field_name}>[^<]*</${field_name}>" | grep -o "true\\|false" | head -1 || echo "")
|
|
119
|
+
|
|
120
|
+
if [[ "$field_value" == "true" ]]; then
|
|
121
|
+
if [[ "$old_opts" =~ e ]]; then set -e; fi
|
|
122
|
+
echo "true"
|
|
123
|
+
return 0
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
# If field is explicitly false OR field is omitted entirely, treat as disabled.
|
|
127
|
+
# Salesforce omits boolean settings fields when they're off rather than
|
|
128
|
+
# emitting <field>false</field>, so an absent field inside an otherwise valid
|
|
129
|
+
# readMetadataResponse is the disabled state, not an error.
|
|
130
|
+
if [[ "$field_value" == "false" ]] || [[ -z "$field_value" ]]; then
|
|
131
|
+
if [[ "$old_opts" =~ e ]]; then set -e; fi
|
|
132
|
+
echo "false"
|
|
133
|
+
return 0
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
# Shouldn't reach here, but if we do, treat as network error (unexpected response format)
|
|
137
|
+
if [[ "$old_opts" =~ e ]]; then set -e; fi
|
|
138
|
+
echo "NETWORK_ERROR"
|
|
139
|
+
return 0
|
|
140
|
+
}
|
package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/stage-descriptions.sh
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# shared/stage-descriptions.sh
|
|
3
|
+
# ---------------------------------------------------------------------------
|
|
4
|
+
# OpptStageDescription creation, extracted from setup-all.sh so it can run in
|
|
5
|
+
# Block A (Call 1) BEFORE the StageName prompt is deployed / activated / tested,
|
|
6
|
+
# rather than as a trailing phase. Stage descriptions are the grounding source
|
|
7
|
+
# for StageName suggestions, so they must exist before the StageName prompt is
|
|
8
|
+
# verified.
|
|
9
|
+
#
|
|
10
|
+
# Sourced by setup-all.sh. Relies on the parent script's helpers (log_*, section,
|
|
11
|
+
# confirm, sanitize_dev_name, track_issue) — bash resolves these at call time, so
|
|
12
|
+
# they only need to be defined before run_stage_descriptions is CALLED (they are).
|
|
13
|
+
#
|
|
14
|
+
# Inputs (globals set by the caller): ORG_ALIAS, NON_INTERACTIVE,
|
|
15
|
+
# CREATE_STAGE_DESCRIPTIONS, STAGENAME_SELECTED, SCRIPT_DIR.
|
|
16
|
+
# Output (global for the Phase 8.5 summary): STAGE_DESCRIPTIONS_BLOCKED.
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
# Generate a context-aware description for custom stages
|
|
20
|
+
# Args: $1=stage_label, $2=methodology_name, $3=record_type_name (optional)
|
|
21
|
+
generate_custom_stage_desc() {
|
|
22
|
+
local stage_label="$1" meth_name="$2" rt_name="${3:-}"
|
|
23
|
+
local stage_lower context_hint meth_hint
|
|
24
|
+
|
|
25
|
+
stage_lower=$(printf '%s' "$stage_label" | tr '[:upper:]' '[:lower:]')
|
|
26
|
+
|
|
27
|
+
if [[ $stage_lower =~ (technical|tech|solution|demo|poc|pilot|trial) ]]; then
|
|
28
|
+
context_hint="Technical validation stage. Activities: solution demonstration, technical requirements review, feasibility assessment, stakeholder technical approval."
|
|
29
|
+
elif [[ $stage_lower =~ (legal|contract|compliance|security|review|audit) ]]; then
|
|
30
|
+
context_hint="Legal/compliance review stage. Activities: contract review, legal approval, security assessment, compliance validation, risk evaluation."
|
|
31
|
+
elif [[ $stage_lower =~ (executive|c-level|leadership|sponsor|approval) ]]; then
|
|
32
|
+
context_hint="Executive approval stage. Activities: executive presentation, C-level buy-in, budget approval, strategic alignment validation."
|
|
33
|
+
elif [[ $stage_lower =~ (procurement|purchasing|vendor|sourcing) ]]; then
|
|
34
|
+
context_hint="Procurement process stage. Activities: vendor evaluation, procurement approval, purchase order creation, budget allocation."
|
|
35
|
+
elif [[ $stage_lower =~ (implementation|onboard|kickoff|setup|deploy) ]]; then
|
|
36
|
+
context_hint="Implementation planning stage. Activities: project kickoff, resource allocation, timeline planning, onboarding preparation."
|
|
37
|
+
elif [[ $stage_lower =~ (finance|financial|budget|pricing|cost) ]]; then
|
|
38
|
+
context_hint="Financial validation stage. Activities: budget confirmation, ROI analysis, cost-benefit review, financial approval."
|
|
39
|
+
else
|
|
40
|
+
context_hint="Custom sales stage. Activities: progress opportunity through this specific phase, document key milestones and decisions."
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
case "$meth_name" in
|
|
44
|
+
MEDDIC)
|
|
45
|
+
meth_hint="Apply MEDDIC framework: Validate Metrics (quantified impact), engage Economic Buyer, confirm Decision Criteria met, understand Decision Process steps, address Pain points, enable Champion support."
|
|
46
|
+
;;
|
|
47
|
+
BANT)
|
|
48
|
+
meth_hint="Apply BANT criteria: Confirm Budget available, validate Authority engaged, ensure Need remains urgent, verify Timeline realistic for this stage."
|
|
49
|
+
;;
|
|
50
|
+
SPICED)
|
|
51
|
+
meth_hint="Apply SPICED framework: Understand Situation context, quantify Pain severity, identify all Impacted stakeholders, validate Critical Event timing, navigate Decision process."
|
|
52
|
+
;;
|
|
53
|
+
*)
|
|
54
|
+
meth_hint="Document progress and next steps aligned to your sales methodology."
|
|
55
|
+
;;
|
|
56
|
+
esac
|
|
57
|
+
|
|
58
|
+
if [[ -n "$rt_name" ]]; then
|
|
59
|
+
printf '%s' "Custom stage for $rt_name: $stage_label. $context_hint $meth_hint Entry: Previous stage criteria met. Exit: Stage-specific requirements completed to advance."
|
|
60
|
+
else
|
|
61
|
+
printf '%s' "Custom stage: $stage_label. $context_hint $meth_hint Entry: Previous stage criteria met. Exit: Stage-specific requirements completed to advance."
|
|
62
|
+
fi
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# run_stage_descriptions — relocated Phase 6 body; self-gates on STAGENAME_SELECTED.
|
|
66
|
+
run_stage_descriptions() {
|
|
67
|
+
section "Phase 4c.5: Stage Descriptions (before StageName prompt)"
|
|
68
|
+
|
|
69
|
+
# OpptStageDescription requires PM fully enabled (Tooling API only)
|
|
70
|
+
DESC_QUERY=$(sf data query --query "SELECT Id, OpportunityStageApiName, Description FROM OpptStageDescription" --target-org "$ORG_ALIAS" --use-tooling-api --json 2>/dev/null || echo '{"status":1}')
|
|
71
|
+
|
|
72
|
+
# Detect inaccessibility (PM not fully provisioned yet)
|
|
73
|
+
if echo "$DESC_QUERY" | grep -qi "INVALID_TYPE\|NOT_FOUND\|sObject type.*not supported"; then
|
|
74
|
+
log_warn "OpptStageDescription not accessible yet (PM provisioning may still be in progress)"
|
|
75
|
+
echo " Stage descriptions can be configured after provisioning completes."
|
|
76
|
+
echo " Re-run: bash setup-all.sh $ORG_ALIAS (in 2-5 minutes)"
|
|
77
|
+
DESC_COUNT="0"
|
|
78
|
+
STAGE_DESCRIPTIONS_BLOCKED="true"
|
|
79
|
+
else
|
|
80
|
+
DESC_COUNT=$(echo "$DESC_QUERY" | jq -r '.result.totalSize // 0' 2>/dev/null | head -1 | tr -cd '0-9' || echo "0")
|
|
81
|
+
DESC_COUNT="${DESC_COUNT:-0}"
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
if [[ "${STAGE_DESCRIPTIONS_BLOCKED:-}" == "true" ]]; then
|
|
85
|
+
: # Already reported - skip Phase 6 entirely
|
|
86
|
+
elif [[ "$STAGENAME_SELECTED" != "true" ]]; then
|
|
87
|
+
# StageName was not among the up-front-selected fields, so stage descriptions
|
|
88
|
+
# are irrelevant this run — they only feed StageName suggestions. Skip the
|
|
89
|
+
# whole gate rather than ask an irrelevant question (3-phase requirement:
|
|
90
|
+
# stage descriptions asked ONLY when StageName is selected).
|
|
91
|
+
log_pass "Stage descriptions: not needed (StageName not selected)"
|
|
92
|
+
elif [[ "$DESC_COUNT" -gt 0 ]]; then
|
|
93
|
+
log_pass "Stage descriptions: $DESC_COUNT configured"
|
|
94
|
+
else
|
|
95
|
+
# No stage descriptions found, and StageName IS selected.
|
|
96
|
+
# CONSENT GATE (#11): stage descriptions are ONLY needed for StageName suggestions and
|
|
97
|
+
# creating them may overwrite descriptions the org intentionally left blank. Creation is
|
|
98
|
+
# therefore opt-in: gated on --create-stage-descriptions (non-interactive) or an explicit
|
|
99
|
+
# interactive confirmation. We NEVER silently default to MEDDIC in non-interactive mode.
|
|
100
|
+
log_info "No stage descriptions configured (StageName is selected — needed for its suggestions)."
|
|
101
|
+
|
|
102
|
+
WANT_STAGE_DESCRIPTIONS=false
|
|
103
|
+
if [[ "$CREATE_STAGE_DESCRIPTIONS" == "true" ]]; then
|
|
104
|
+
WANT_STAGE_DESCRIPTIONS=true
|
|
105
|
+
elif [[ "$NON_INTERACTIVE" != "true" ]]; then
|
|
106
|
+
if confirm "Create stage descriptions now? (only needed if you want StageName suggestions)"; then
|
|
107
|
+
WANT_STAGE_DESCRIPTIONS=true
|
|
108
|
+
fi
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
if [[ "$WANT_STAGE_DESCRIPTIONS" != "true" ]]; then
|
|
112
|
+
log_pass "Stage descriptions: skipped (not requested)"
|
|
113
|
+
if [[ "$NON_INTERACTIVE" == "true" ]]; then
|
|
114
|
+
echo " To create them, re-run with --create-stage-descriptions"
|
|
115
|
+
fi
|
|
116
|
+
METHODOLOGY="4" # sentinel: skip the creation branch below
|
|
117
|
+
elif [[ "$NON_INTERACTIVE" == "true" ]]; then
|
|
118
|
+
# Consent given via flag, but no TTY to choose a methodology — default to MEDDIC and say so.
|
|
119
|
+
echo " Creating stage descriptions with MEDDIC methodology (default for --create-stage-descriptions)..."
|
|
120
|
+
METHODOLOGY="1"
|
|
121
|
+
else
|
|
122
|
+
echo " Which sales methodology?"
|
|
123
|
+
echo " 1) MEDDIC 2) BANT 3) SPICED 4) Skip"
|
|
124
|
+
read -rp " Choice (1-4): " METHODOLOGY
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
if [[ "$METHODOLOGY" =~ ^[1-3]$ ]]; then
|
|
128
|
+
# Map methodology choice to file name
|
|
129
|
+
case "$METHODOLOGY" in
|
|
130
|
+
1) METH_NAME="MEDDIC"; METH_FILE="meddic.txt" ;;
|
|
131
|
+
2) METH_NAME="BANT"; METH_FILE="bant.txt" ;;
|
|
132
|
+
3) METH_NAME="SPICED"; METH_FILE="spiced.txt" ;;
|
|
133
|
+
esac
|
|
134
|
+
|
|
135
|
+
# Load stage descriptions from asset file
|
|
136
|
+
DESCRIPTIONS_FILE="$SCRIPT_DIR/../assets/stage-descriptions/$METH_FILE"
|
|
137
|
+
if [[ ! -f "$DESCRIPTIONS_FILE" ]]; then
|
|
138
|
+
log_fail "Stage description file not found: $DESCRIPTIONS_FILE"
|
|
139
|
+
log_warn "Skipping stage description creation."
|
|
140
|
+
else
|
|
141
|
+
log_info "Creating stage descriptions using $METH_NAME methodology..."
|
|
142
|
+
|
|
143
|
+
# Define the 11 standard OOTB stages (for custom stage detection)
|
|
144
|
+
STANDARD_STAGES=(
|
|
145
|
+
"Prospecting"
|
|
146
|
+
"Qualification"
|
|
147
|
+
"Needs Analysis"
|
|
148
|
+
"Value Proposition"
|
|
149
|
+
"Id. Decision Makers"
|
|
150
|
+
"Perception Analysis"
|
|
151
|
+
"Proposal/Price Quote"
|
|
152
|
+
"Negotiation/Review"
|
|
153
|
+
"Closed Won"
|
|
154
|
+
"Closed Lost"
|
|
155
|
+
"Most Likely"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Count active Opportunity record types to decide which log path to take.
|
|
159
|
+
# OpptStageDescription is global-per-stage regardless — we only use this count
|
|
160
|
+
# for the log message, not to fan out the write loop.
|
|
161
|
+
RT_QUERY=$(sf data query --query "SELECT COUNT() FROM RecordType WHERE SObjectType = 'Opportunity' AND IsActive = true" --target-org "$ORG_ALIAS" --json 2>/dev/null)
|
|
162
|
+
RT_COUNT=$(echo "$RT_QUERY" | jq -r '.result.totalSize // 0' 2>/dev/null | head -1 | tr -cd '0-9' || echo "0")
|
|
163
|
+
RT_COUNT="${RT_COUNT:-0}"
|
|
164
|
+
|
|
165
|
+
if [[ "$RT_COUNT" -eq 0 ]]; then
|
|
166
|
+
log_warn "No Opportunity record types found. Using global stage picklist values."
|
|
167
|
+
# Fall back to global picklist query (no record type context)
|
|
168
|
+
STAGE_QUERY=$(sf data query --query "SELECT MasterLabel, ApiName FROM OpportunityStage WHERE IsActive=true ORDER BY SortOrder" --target-org "$ORG_ALIAS" --json 2>/dev/null)
|
|
169
|
+
|
|
170
|
+
# Batch pre-fetch existing stage descriptions (avoid N+1 queries)
|
|
171
|
+
EXISTING_DESCS=$(sf data query -q "SELECT Id, OpportunityStageApiName, Description FROM OpptStageDescription" --target-org "$ORG_ALIAS" --use-tooling-api --json 2>/dev/null || echo '{"result":{"records":[]}}')
|
|
172
|
+
|
|
173
|
+
CREATED=0
|
|
174
|
+
FAILED=0 # real API create/update failures only
|
|
175
|
+
NO_DESC=0 # methodology file had no line for this stage (not an API error)
|
|
176
|
+
CUSTOM_SKIPPED=0
|
|
177
|
+
|
|
178
|
+
while IFS= read -r STAGE_JSON; do
|
|
179
|
+
[[ -z "$STAGE_JSON" ]] && continue
|
|
180
|
+
STAGE_LABEL=$(echo "$STAGE_JSON" | jq -r '.MasterLabel // empty' 2>/dev/null)
|
|
181
|
+
STAGE_API=$(echo "$STAGE_JSON" | jq -r '.ApiName // empty' 2>/dev/null)
|
|
182
|
+
[[ -z "$STAGE_LABEL" || -z "$STAGE_API" ]] && continue
|
|
183
|
+
|
|
184
|
+
# Check if this is a standard stage
|
|
185
|
+
IS_STANDARD=false
|
|
186
|
+
for STD_STAGE in "${STANDARD_STAGES[@]}"; do
|
|
187
|
+
if [[ "$STAGE_LABEL" == "$STD_STAGE" ]]; then
|
|
188
|
+
IS_STANDARD=true
|
|
189
|
+
break
|
|
190
|
+
fi
|
|
191
|
+
done
|
|
192
|
+
|
|
193
|
+
if [[ "$IS_STANDARD" == "false" ]]; then
|
|
194
|
+
# Custom stage detected
|
|
195
|
+
echo ""
|
|
196
|
+
log_info "Custom stage detected: $STAGE_LABEL"
|
|
197
|
+
|
|
198
|
+
RECOMMENDED_DESC=$(generate_custom_stage_desc "$STAGE_LABEL" "$METH_NAME")
|
|
199
|
+
|
|
200
|
+
if [[ ! -t 0 ]]; then
|
|
201
|
+
# Non-interactive: skip custom stages with warning
|
|
202
|
+
log_warn "Non-interactive mode: skipping custom stage '$STAGE_LABEL'"
|
|
203
|
+
CUSTOM_SKIPPED=$((CUSTOM_SKIPPED + 1))
|
|
204
|
+
continue
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
echo " Recommended description:"
|
|
208
|
+
echo " $RECOMMENDED_DESC"
|
|
209
|
+
echo ""
|
|
210
|
+
read -rp " Use this description? (y/n/edit): " CUSTOM_CHOICE
|
|
211
|
+
|
|
212
|
+
case "$CUSTOM_CHOICE" in
|
|
213
|
+
y|Y|yes|Yes|YES)
|
|
214
|
+
DESC="$RECOMMENDED_DESC"
|
|
215
|
+
;;
|
|
216
|
+
e|E|edit|Edit|EDIT)
|
|
217
|
+
echo " Enter custom description (single line):"
|
|
218
|
+
read -r DESC
|
|
219
|
+
[[ -z "$DESC" ]] && { log_warn "Empty description - skipping stage"; continue; }
|
|
220
|
+
;;
|
|
221
|
+
*)
|
|
222
|
+
log_warn "Skipping custom stage: $STAGE_LABEL"
|
|
223
|
+
CUSTOM_SKIPPED=$((CUSTOM_SKIPPED + 1))
|
|
224
|
+
continue
|
|
225
|
+
;;
|
|
226
|
+
esac
|
|
227
|
+
else
|
|
228
|
+
# Standard stage - load from file
|
|
229
|
+
# Literal match on the first pipe-delimited field. A stage label can
|
|
230
|
+
# contain regex metacharacters (the OOTB "Id. Decision Makers" has a
|
|
231
|
+
# '.'), so grep "^LABEL|" would treat them as patterns and mis-match.
|
|
232
|
+
DESC=$(awk -F'|' -v s="$STAGE_LABEL" '$1==s{sub(/^[^|]*\|/,""); print; exit}' "$DESCRIPTIONS_FILE")
|
|
233
|
+
if [[ -z "$DESC" ]]; then
|
|
234
|
+
log_info "No ${METH_NAME} description defined for standard stage '$STAGE_LABEL' — skipping (not an error)."
|
|
235
|
+
NO_DESC=$((NO_DESC + 1))
|
|
236
|
+
continue
|
|
237
|
+
fi
|
|
238
|
+
fi
|
|
239
|
+
|
|
240
|
+
# Create/update OpptStageDescription record. Use the sanitizer so labels like
|
|
241
|
+
# "Id. Decision Makers" yield a VALID DeveloperName (not "Id__Decision_Makers").
|
|
242
|
+
DEV_NAME=$(sanitize_dev_name "$STAGE_LABEL")
|
|
243
|
+
|
|
244
|
+
# Check if description already exists for this stage
|
|
245
|
+
EXISTING_ID=$(printf '%s' "$EXISTING_DESCS" | jq -r --arg stage "$STAGE_API" '.result.records[] | select(.OpportunityStageApiName == $stage) | .Id' 2>/dev/null | head -1)
|
|
246
|
+
EXISTING_ID="${EXISTING_ID:-}"
|
|
247
|
+
|
|
248
|
+
# Idempotency: for custom stages with existing descriptions, offer to keep
|
|
249
|
+
if [[ "$IS_STANDARD" == "false" && -n "$EXISTING_ID" && -t 0 ]]; then
|
|
250
|
+
CURRENT_DESC=$(printf '%s' "$EXISTING_DESCS" | jq -r --arg id "$EXISTING_ID" '.result.records[] | select(.Id == $id) | .Description // ""' 2>/dev/null || echo "")
|
|
251
|
+
if [[ -n "$CURRENT_DESC" ]]; then
|
|
252
|
+
echo " Current: ${CURRENT_DESC:0:100}"
|
|
253
|
+
read -rp " Keep current description? (y/n): " KEEP_CHOICE
|
|
254
|
+
if [[ "$KEEP_CHOICE" =~ ^[yY] ]]; then
|
|
255
|
+
CREATED=$((CREATED + 1))
|
|
256
|
+
continue
|
|
257
|
+
fi
|
|
258
|
+
fi
|
|
259
|
+
fi
|
|
260
|
+
|
|
261
|
+
# Escape for SOQL/CLI: double single-quotes, strip backticks and shell metacharacters
|
|
262
|
+
DESC_ESCAPED=$(printf '%s' "$DESC" | sed "s/'/''/g" | tr -d '`' | sed 's/\$/\\$/g')
|
|
263
|
+
|
|
264
|
+
if [[ -n "$EXISTING_ID" ]]; then
|
|
265
|
+
# Update existing record — capture stderr+json so a failure reports WHY.
|
|
266
|
+
SD_RESULT=$(sf data update record --sobject OpptStageDescription --record-id "$EXISTING_ID" \
|
|
267
|
+
--values "Description='${DESC_ESCAPED}'" \
|
|
268
|
+
--target-org "$ORG_ALIAS" --use-tooling-api --json 2>&1 || true)
|
|
269
|
+
if echo "$SD_RESULT" | jq -e '.result.id // .status == 0' >/dev/null 2>&1; then
|
|
270
|
+
CREATED=$((CREATED + 1))
|
|
271
|
+
else
|
|
272
|
+
SD_ERR=$(echo "$SD_RESULT" | jq -r '.message // .result.message // empty' 2>/dev/null || echo "")
|
|
273
|
+
log_fail "Failed to update '$STAGE_LABEL' (DeveloperName='$DEV_NAME'): ${SD_ERR:-$SD_RESULT}"
|
|
274
|
+
FAILED=$((FAILED + 1))
|
|
275
|
+
fi
|
|
276
|
+
else
|
|
277
|
+
# Create new record — capture stderr+json so a failure reports WHY.
|
|
278
|
+
SD_RESULT=$(sf data create record --sobject OpptStageDescription \
|
|
279
|
+
--values "DeveloperName='${DEV_NAME}' MasterLabel='${STAGE_LABEL}' OpportunityStageApiName='${STAGE_API}' Description='${DESC_ESCAPED}'" \
|
|
280
|
+
--target-org "$ORG_ALIAS" --use-tooling-api --json 2>&1 || true)
|
|
281
|
+
if echo "$SD_RESULT" | jq -e '.result.id // empty' >/dev/null 2>&1; then
|
|
282
|
+
CREATED=$((CREATED + 1))
|
|
283
|
+
else
|
|
284
|
+
SD_ERR=$(echo "$SD_RESULT" | jq -r '.message // .result.message // empty' 2>/dev/null || echo "")
|
|
285
|
+
log_fail "Failed to create '$STAGE_LABEL' (DeveloperName='$DEV_NAME'): ${SD_ERR:-$SD_RESULT}"
|
|
286
|
+
FAILED=$((FAILED + 1))
|
|
287
|
+
fi
|
|
288
|
+
fi
|
|
289
|
+
done < <(echo "$STAGE_QUERY" | jq -c '.result.records[]?' 2>/dev/null)
|
|
290
|
+
|
|
291
|
+
# Summary — report each category distinctly so real API failures are not
|
|
292
|
+
# conflated with stages that simply had no methodology line (#8).
|
|
293
|
+
if [[ $FAILED -gt 0 ]]; then
|
|
294
|
+
log_fail "Stage descriptions: $CREATED created/updated, $FAILED FAILED (see errors above), $NO_DESC no-description, $CUSTOM_SKIPPED custom skipped"
|
|
295
|
+
elif [[ $CREATED -gt 0 ]]; then
|
|
296
|
+
log_pass "Stage descriptions: $CREATED created/updated, $NO_DESC no-description, $CUSTOM_SKIPPED custom skipped"
|
|
297
|
+
else
|
|
298
|
+
log_warn "Stage descriptions: none created ($NO_DESC no-description, $CUSTOM_SKIPPED custom skipped)"
|
|
299
|
+
fi
|
|
300
|
+
|
|
301
|
+
else
|
|
302
|
+
# Record types exist. OpptStageDescription is GLOBAL-PER-STAGE (keyed only by
|
|
303
|
+
# OpportunityStageApiName — there is no OpportunityRecordTypeId column on the
|
|
304
|
+
# object), so each active stage is written exactly ONCE regardless of how many
|
|
305
|
+
# Opportunity record types exist. Descriptions are NOT partitioned by record
|
|
306
|
+
# type, so we write against the global active-stage picklist directly. (W-23356857)
|
|
307
|
+
log_info "Found $RT_COUNT opportunity record type(s). Writing global-per-stage descriptions (one per active stage)..."
|
|
308
|
+
|
|
309
|
+
TOTAL_CREATED=0
|
|
310
|
+
TOTAL_FAILED=0
|
|
311
|
+
TOTAL_NO_DESC=0
|
|
312
|
+
TOTAL_CUSTOM_SKIPPED=0
|
|
313
|
+
|
|
314
|
+
# Batch pre-fetch ALL existing stage descriptions once (avoid redundant queries per stage)
|
|
315
|
+
EXISTING_DESCS=$(sf data query -q "SELECT Id, OpportunityStageApiName, Description FROM OpptStageDescription" --target-org "$ORG_ALIAS" --use-tooling-api --json 2>/dev/null || echo '{"result":{"records":[]}}')
|
|
316
|
+
|
|
317
|
+
# OpptStageDescription is global-per-stage: iterate the global active-stage picklist
|
|
318
|
+
# once and write one description per stage (no per-record-type partitioning).
|
|
319
|
+
RT_STAGES_QUERY=$(sf data query --query "SELECT MasterLabel, ApiName FROM OpportunityStage WHERE IsActive=true ORDER BY SortOrder" --target-org "$ORG_ALIAS" --json 2>/dev/null)
|
|
320
|
+
|
|
321
|
+
while IFS= read -r STAGE_JSON; do
|
|
322
|
+
[[ -z "$STAGE_JSON" ]] && continue
|
|
323
|
+
STAGE_LABEL=$(echo "$STAGE_JSON" | jq -r '.MasterLabel // empty' 2>/dev/null)
|
|
324
|
+
STAGE_API=$(echo "$STAGE_JSON" | jq -r '.ApiName // empty' 2>/dev/null)
|
|
325
|
+
[[ -z "$STAGE_LABEL" || -z "$STAGE_API" ]] && continue
|
|
326
|
+
|
|
327
|
+
# Check if this is a standard stage
|
|
328
|
+
IS_STANDARD=false
|
|
329
|
+
for STD_STAGE in "${STANDARD_STAGES[@]}"; do
|
|
330
|
+
if [[ "$STAGE_LABEL" == "$STD_STAGE" ]]; then
|
|
331
|
+
IS_STANDARD=true
|
|
332
|
+
break
|
|
333
|
+
fi
|
|
334
|
+
done
|
|
335
|
+
|
|
336
|
+
if [[ "$IS_STANDARD" == "false" ]]; then
|
|
337
|
+
# Custom stage detected
|
|
338
|
+
log_info " Custom stage: $STAGE_LABEL"
|
|
339
|
+
|
|
340
|
+
RECOMMENDED_DESC=$(generate_custom_stage_desc "$STAGE_LABEL" "$METH_NAME")
|
|
341
|
+
|
|
342
|
+
if [[ ! -t 0 ]]; then
|
|
343
|
+
# Non-interactive: skip custom stages with warning
|
|
344
|
+
log_warn " Non-interactive mode: skipping custom stage '$STAGE_LABEL'"
|
|
345
|
+
TOTAL_CUSTOM_SKIPPED=$((TOTAL_CUSTOM_SKIPPED + 1))
|
|
346
|
+
continue
|
|
347
|
+
fi
|
|
348
|
+
|
|
349
|
+
echo " Recommended description:"
|
|
350
|
+
echo " $RECOMMENDED_DESC"
|
|
351
|
+
echo ""
|
|
352
|
+
read -rp " Use this description? (y/n/edit): " CUSTOM_CHOICE
|
|
353
|
+
|
|
354
|
+
case "$CUSTOM_CHOICE" in
|
|
355
|
+
y|Y|yes|Yes|YES)
|
|
356
|
+
DESC="$RECOMMENDED_DESC"
|
|
357
|
+
;;
|
|
358
|
+
e|E|edit|Edit|EDIT)
|
|
359
|
+
echo " Enter custom description (single line):"
|
|
360
|
+
read -r DESC
|
|
361
|
+
[[ -z "$DESC" ]] && { log_warn " Empty description - skipping stage"; continue; }
|
|
362
|
+
;;
|
|
363
|
+
*)
|
|
364
|
+
log_warn " Skipping custom stage: $STAGE_LABEL"
|
|
365
|
+
TOTAL_CUSTOM_SKIPPED=$((TOTAL_CUSTOM_SKIPPED + 1))
|
|
366
|
+
continue
|
|
367
|
+
;;
|
|
368
|
+
esac
|
|
369
|
+
else
|
|
370
|
+
# Standard stage - load from file
|
|
371
|
+
# Literal match on the first pipe-delimited field. A stage label can
|
|
372
|
+
# contain regex metacharacters (the OOTB "Id. Decision Makers" has a
|
|
373
|
+
# '.'), so grep "^LABEL|" would treat them as patterns and mis-match.
|
|
374
|
+
DESC=$(awk -F'|' -v s="$STAGE_LABEL" '$1==s{sub(/^[^|]*\|/,""); print; exit}' "$DESCRIPTIONS_FILE")
|
|
375
|
+
if [[ -z "$DESC" ]]; then
|
|
376
|
+
log_info " No ${METH_NAME} description defined for standard stage '$STAGE_LABEL' — skipping (not an error)."
|
|
377
|
+
TOTAL_NO_DESC=$((TOTAL_NO_DESC + 1))
|
|
378
|
+
continue
|
|
379
|
+
fi
|
|
380
|
+
fi
|
|
381
|
+
|
|
382
|
+
# Create/update the global OpptStageDescription record (keyed by stage only).
|
|
383
|
+
# Sanitizer guards against invalid DeveloperNames (e.g. double underscores).
|
|
384
|
+
DEV_NAME=$(sanitize_dev_name "$STAGE_LABEL")
|
|
385
|
+
|
|
386
|
+
# Check if a description already exists for this stage.
|
|
387
|
+
EXISTING_ID=$(printf '%s' "$EXISTING_DESCS" | jq -r --arg stage "$STAGE_API" '.result.records[] | select(.OpportunityStageApiName == $stage) | .Id' 2>/dev/null | head -1)
|
|
388
|
+
EXISTING_ID="${EXISTING_ID:-}"
|
|
389
|
+
|
|
390
|
+
# Idempotency: for custom stages with existing descriptions, offer to keep
|
|
391
|
+
if [[ "$IS_STANDARD" == "false" && -n "$EXISTING_ID" && -t 0 ]]; then
|
|
392
|
+
CURRENT_DESC=$(printf '%s' "$EXISTING_DESCS" | jq -r --arg id "$EXISTING_ID" '.result.records[] | select(.Id == $id) | .Description // ""' 2>/dev/null || echo "")
|
|
393
|
+
if [[ -n "$CURRENT_DESC" ]]; then
|
|
394
|
+
echo " Current: ${CURRENT_DESC:0:100}"
|
|
395
|
+
read -rp " Keep current description? (y/n): " KEEP_CHOICE
|
|
396
|
+
if [[ "$KEEP_CHOICE" =~ ^[yY] ]]; then
|
|
397
|
+
TOTAL_CREATED=$((TOTAL_CREATED + 1))
|
|
398
|
+
continue
|
|
399
|
+
fi
|
|
400
|
+
fi
|
|
401
|
+
fi
|
|
402
|
+
|
|
403
|
+
# Escape for SOQL/CLI: double single-quotes, strip backticks and shell metacharacters
|
|
404
|
+
DESC_ESCAPED=$(printf '%s' "$DESC" | sed "s/'/''/g" | tr -d '`' | sed 's/\$/\\$/g')
|
|
405
|
+
|
|
406
|
+
if [[ -n "$EXISTING_ID" ]]; then
|
|
407
|
+
# Update existing record — capture the error on failure.
|
|
408
|
+
SD_RESULT=$(sf data update record --sobject OpptStageDescription --record-id "$EXISTING_ID" \
|
|
409
|
+
--values "Description='${DESC_ESCAPED}'" \
|
|
410
|
+
--target-org "$ORG_ALIAS" --use-tooling-api --json 2>&1 || true)
|
|
411
|
+
if echo "$SD_RESULT" | jq -e '.result.id // .status == 0' >/dev/null 2>&1; then
|
|
412
|
+
TOTAL_CREATED=$((TOTAL_CREATED + 1))
|
|
413
|
+
else
|
|
414
|
+
SD_ERR=$(echo "$SD_RESULT" | jq -r '.message // .result.message // empty' 2>/dev/null || echo "")
|
|
415
|
+
log_fail " Failed to update '$STAGE_LABEL' (DeveloperName='$DEV_NAME'): ${SD_ERR:-$SD_RESULT}"
|
|
416
|
+
TOTAL_FAILED=$((TOTAL_FAILED + 1))
|
|
417
|
+
fi
|
|
418
|
+
else
|
|
419
|
+
# Create new global record — capture the error on failure.
|
|
420
|
+
SD_RESULT=$(sf data create record --sobject OpptStageDescription \
|
|
421
|
+
--values "DeveloperName='${DEV_NAME}' MasterLabel='${STAGE_LABEL}' OpportunityStageApiName='${STAGE_API}' Description='${DESC_ESCAPED}'" \
|
|
422
|
+
--target-org "$ORG_ALIAS" --use-tooling-api --json 2>&1 || true)
|
|
423
|
+
if echo "$SD_RESULT" | jq -e '.result.id // empty' >/dev/null 2>&1; then
|
|
424
|
+
TOTAL_CREATED=$((TOTAL_CREATED + 1))
|
|
425
|
+
else
|
|
426
|
+
SD_ERR=$(echo "$SD_RESULT" | jq -r '.message // .result.message // empty' 2>/dev/null || echo "")
|
|
427
|
+
log_fail " Failed to create '$STAGE_LABEL' (DeveloperName='$DEV_NAME'): ${SD_ERR:-$SD_RESULT}"
|
|
428
|
+
TOTAL_FAILED=$((TOTAL_FAILED + 1))
|
|
429
|
+
fi
|
|
430
|
+
fi
|
|
431
|
+
done < <(echo "$RT_STAGES_QUERY" | jq -c '.result.records[]?' 2>/dev/null)
|
|
432
|
+
|
|
433
|
+
# Overall summary — real API failures reported distinctly from no-description skips.
|
|
434
|
+
echo ""
|
|
435
|
+
if [[ $TOTAL_FAILED -gt 0 ]]; then
|
|
436
|
+
log_fail "Stage descriptions: $TOTAL_CREATED created/updated, $TOTAL_FAILED FAILED (see errors above), ${TOTAL_NO_DESC:-0} no-description, $TOTAL_CUSTOM_SKIPPED custom skipped"
|
|
437
|
+
elif [[ $TOTAL_CREATED -gt 0 ]]; then
|
|
438
|
+
log_pass "Stage descriptions: $TOTAL_CREATED created/updated, ${TOTAL_NO_DESC:-0} no-description, $TOTAL_CUSTOM_SKIPPED custom skipped"
|
|
439
|
+
else
|
|
440
|
+
log_warn "Stage descriptions: none created (${TOTAL_NO_DESC:-0} no-description, $TOTAL_CUSTOM_SKIPPED custom skipped)"
|
|
441
|
+
fi
|
|
442
|
+
fi
|
|
443
|
+
fi
|
|
444
|
+
else
|
|
445
|
+
log_warn "Skipped. Stage suggestions will NOT work without descriptions."
|
|
446
|
+
fi
|
|
447
|
+
fi
|
|
448
|
+
}
|