@salesforce/afv-skills 1.31.0 → 1.32.0

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