@salesforce/afv-skills 1.47.0 → 1.49.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 (160) hide show
  1. package/package.json +1 -1
  2. package/skills/education-cloud-domain-configure/SKILL.md +234 -0
  3. package/skills/education-cloud-domain-configure/references/dependent-features.md +198 -0
  4. package/skills/education-cloud-domain-configure/references/domain-features/academic-operations.md +386 -0
  5. package/skills/education-cloud-domain-configure/references/domain-features/alumni-relations.md +155 -0
  6. package/skills/education-cloud-domain-configure/references/domain-features/cross-domain-dependencies.md +18 -0
  7. package/skills/education-cloud-domain-configure/references/domain-features/industriessettings-toggle-reference.md +43 -0
  8. package/skills/education-cloud-domain-configure/references/domain-features/mentoring.md +198 -0
  9. package/skills/education-cloud-domain-configure/references/domain-features/recruitment-admissions.md +315 -0
  10. package/skills/education-cloud-domain-configure/references/domain-features/student-management.md +155 -0
  11. package/skills/education-cloud-domain-configure/references/domain-features/student-success.md +249 -0
  12. package/skills/education-cloud-domain-configure/references/domain-features.md +27 -0
  13. package/skills/education-cloud-domain-configure/references/gotchas-extended.md +23 -0
  14. package/skills/education-cloud-domain-configure/references/toggle-write-path.md +67 -0
  15. package/skills/education-cloud-domain-configure/scripts/check_license_provisioned.py +62 -0
  16. package/skills/education-cloud-domain-configure/scripts/check_org_edition.py +44 -0
  17. package/skills/education-cloud-domain-configure/scripts/check_permission_assigned.py +39 -0
  18. package/skills/education-cloud-multi-campus-configure/SKILL.md +2 -0
  19. package/skills/education-cloud-multi-campus-configure/references/foundation_prerequisites.md +4 -3
  20. package/skills/service-agentforce-channel-configure/SKILL.md +33 -28
  21. package/skills/service-agentforce-human-escalation-configure/SKILL.md +205 -0
  22. package/skills/service-agentforce-human-escalation-configure/assets/escalation-thresholds.instructions.md +37 -0
  23. package/skills/service-agentforce-human-escalation-configure/assets/force-app/main/default/flows/Human_Escalation_Outbound_Flow.flow-meta.xml +173 -0
  24. package/skills/service-agentforce-human-escalation-configure/assets/package.xml +8 -0
  25. package/skills/service-agentforce-human-escalation-configure/assets/sfdx-project.json +5 -0
  26. package/skills/service-agentforce-human-escalation-configure/references/classifier-contracts.md +103 -0
  27. package/skills/service-agentforce-human-escalation-configure/references/escalation-mechanism.md +63 -0
  28. package/skills/service-agentforce-human-escalation-configure/references/failure-policy.md +44 -0
  29. package/skills/service-agentforce-human-escalation-configure/references/nga-escalation.md +57 -0
  30. package/skills/service-agentforce-human-escalation-configure/references/runtime-verification.md +72 -0
  31. package/skills/service-agentforce-human-escalation-configure/scripts/classify-agent-active.mjs +49 -0
  32. package/skills/service-agentforce-human-escalation-configure/scripts/classify-nga-escalation.mjs +166 -0
  33. package/skills/service-agentforce-human-escalation-configure/scripts/extract-outbound-route.mjs +67 -0
  34. package/skills/service-agentforce-human-escalation-configure/scripts/patch-escalation-surfaces.mjs +131 -0
  35. package/skills/service-agentforce-human-escalation-configure/scripts/tests/__init__.py +0 -0
  36. package/skills/service-agentforce-human-escalation-configure/scripts/tests/_bootstrap.py +249 -0
  37. package/skills/service-agentforce-human-escalation-configure/scripts/tests/test_escalation_contracts.py +899 -0
  38. package/skills/service-agentforce-human-escalation-configure/scripts/verify-and-configure.sh +705 -0
  39. package/skills/service-agentforce-human-escalation-configure/scripts/verify-escalation-config.mjs +89 -0
  40. package/skills/service-de-channel-routing-configure/SKILL.md +13 -5
  41. package/skills/service-de-channel-routing-configure/scripts/tests/test_skill_contract.py +25 -0
  42. package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +23 -10
  43. package/skills/service-itsm-agentic-setup-agentforce-coordinate/examples/output-templates.md +63 -48
  44. package/skills/service-itsm-agentic-setup-agentforce-coordinate/scripts/verify-child-verdict.mjs +6 -2
  45. package/skills/service-omni-agent-users-create/SKILL.md +96 -0
  46. package/skills/service-omni-agent-users-create/assets/create-users.apex.template +80 -0
  47. package/skills/service-omni-agent-users-create/references/apex-patterns.md +135 -0
  48. package/skills/service-omni-agent-users-create/references/apex-template-notes.md +40 -0
  49. package/skills/service-omni-agent-users-create/scripts/detect-and-create.sh +206 -0
  50. package/skills/service-omni-agent-users-create/scripts/detect-existing.sh +122 -0
  51. package/skills/service-omni-agent-users-create/scripts/run-create.sh +298 -0
  52. package/skills/service-omni-agent-users-create/scripts/tests/test_user_create_security.py +171 -0
  53. package/skills/service-omni-agent-work-sharing-configure/SKILL.md +78 -0
  54. package/skills/service-omni-agent-work-sharing-configure/references/api-notes.md +31 -0
  55. package/skills/service-omni-agent-work-sharing-configure/scripts/configure-and-report.sh +186 -0
  56. package/skills/service-omni-agent-work-sharing-configure/scripts/tests/_bootstrap.py +120 -0
  57. package/skills/service-omni-agent-work-sharing-configure/scripts/tests/test_agent_work_sharing_contracts.py +93 -0
  58. package/skills/service-omni-base-settings-configure/SKILL.md +77 -0
  59. package/skills/service-omni-base-settings-configure/assets/force-app/main/default/settings/OmniChannel.settings-meta.xml +8 -0
  60. package/skills/service-omni-base-settings-configure/assets/package.xml +8 -0
  61. package/skills/service-omni-base-settings-configure/assets/sfdx-project.json +12 -0
  62. package/skills/service-omni-base-settings-configure/references/api-notes.md +121 -0
  63. package/skills/service-omni-base-settings-configure/scripts/configure-and-report.sh +164 -0
  64. package/skills/service-omni-base-settings-configure/scripts/detect-existing.sh +69 -0
  65. package/skills/service-omni-channel-setup-coordinate/SKILL.md +168 -0
  66. package/skills/service-omni-channel-setup-coordinate/examples/output-templates.md +161 -0
  67. package/skills/service-omni-channel-setup-coordinate/references/gap-catalog.md +99 -0
  68. package/skills/service-omni-channel-setup-coordinate/scripts/integration-driver.sh +1542 -0
  69. package/skills/service-omni-channel-setup-coordinate/scripts/tests/__init__.py +0 -0
  70. package/skills/service-omni-channel-setup-coordinate/scripts/tests/_bootstrap.py +223 -0
  71. package/skills/service-omni-channel-setup-coordinate/scripts/tests/test_omni_contracts.py +804 -0
  72. package/skills/service-omni-command-center-analyze/SKILL.md +84 -0
  73. package/skills/service-omni-command-center-analyze/references/api-notes.md +54 -0
  74. package/skills/service-omni-command-center-analyze/scripts/analyze.sh +198 -0
  75. package/skills/service-omni-command-center-analyze/scripts/tests/_bootstrap.py +115 -0
  76. package/skills/service-omni-command-center-analyze/scripts/tests/test_command_center_analyze_contracts.py +165 -0
  77. package/skills/service-omni-permission-set-assign/SKILL.md +88 -0
  78. package/skills/service-omni-permission-set-assign/assets/package.xml +8 -0
  79. package/skills/service-omni-permission-set-assign/references/api-notes.md +143 -0
  80. package/skills/service-omni-permission-set-assign/scripts/verify-and-assign.sh +397 -0
  81. package/skills/service-omni-presence-status-deploy/SKILL.md +87 -0
  82. package/skills/service-omni-presence-status-deploy/assets/package.xml +12 -0
  83. package/skills/service-omni-presence-status-deploy/references/api-notes.md +115 -0
  84. package/skills/service-omni-presence-status-deploy/scripts/deploy-and-report.sh +422 -0
  85. package/skills/service-omni-presence-user-config-deploy/SKILL.md +80 -0
  86. package/skills/service-omni-presence-user-config-deploy/references/api-notes.md +60 -0
  87. package/skills/service-omni-presence-user-config-deploy/scripts/deploy-and-report.sh +261 -0
  88. package/skills/service-omni-presence-user-config-deploy/scripts/tests/_bootstrap.py +106 -0
  89. package/skills/service-omni-presence-user-config-deploy/scripts/tests/test_presence_user_config_contracts.py +142 -0
  90. package/skills/service-omni-queue-deploy/SKILL.md +91 -0
  91. package/skills/service-omni-queue-deploy/references/api-notes.md +140 -0
  92. package/skills/service-omni-queue-deploy/scripts/verify-and-align.sh +470 -0
  93. package/skills/service-omni-queue-members-assign/SKILL.md +93 -0
  94. package/skills/service-omni-queue-members-assign/references/api-notes.md +144 -0
  95. package/skills/service-omni-queue-members-assign/scripts/tests/_bootstrap.py +67 -0
  96. package/skills/service-omni-queue-members-assign/scripts/tests/test_queue_members_contracts.py +73 -0
  97. package/skills/service-omni-queue-members-assign/scripts/verify-and-bind.sh +362 -0
  98. package/skills/service-omni-queue-routing-config-deploy/SKILL.md +101 -0
  99. package/skills/service-omni-queue-routing-config-deploy/references/api-notes.md +127 -0
  100. package/skills/service-omni-queue-routing-config-deploy/scripts/tests/_bootstrap.py +90 -0
  101. package/skills/service-omni-queue-routing-config-deploy/scripts/tests/test_queue_routing_config_contracts.py +70 -0
  102. package/skills/service-omni-queue-routing-config-deploy/scripts/upsert-and-report.sh +538 -0
  103. package/skills/service-omni-routing-flow-deploy/SKILL.md +111 -0
  104. package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_Case_Trigger.SkillsBased.flow-meta.xml +94 -0
  105. package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_Case_Trigger.flow-meta.xml +88 -0
  106. package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_Cases.flow-meta.xml +144 -0
  107. package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_VoiceCall_Trigger.SkillsBased.flow-meta.xml +94 -0
  108. package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_VoiceCall_Trigger.flow-meta.xml +88 -0
  109. package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_VoiceCalls.flow-meta.xml +144 -0
  110. package/skills/service-omni-routing-flow-deploy/assets/package.xml +11 -0
  111. package/skills/service-omni-routing-flow-deploy/assets/sfdx-project.json +11 -0
  112. package/skills/service-omni-routing-flow-deploy/references/api-notes.md +265 -0
  113. package/skills/service-omni-routing-flow-deploy/scripts/deploy-and-report.sh +684 -0
  114. package/skills/service-omni-service-channel-configure/SKILL.md +83 -0
  115. package/skills/service-omni-service-channel-configure/assets/force-app/main/default/serviceChannels/Cases.serviceChannel-meta.xml +9 -0
  116. package/skills/service-omni-service-channel-configure/assets/force-app/main/default/serviceChannels/Incidents.serviceChannel-meta.xml +9 -0
  117. package/skills/service-omni-service-channel-configure/assets/force-app/main/default/serviceChannels/MessagingSessions.serviceChannel-meta.xml +9 -0
  118. package/skills/service-omni-service-channel-configure/assets/force-app/main/default/serviceChannels/VoiceCalls.serviceChannel-meta.xml +9 -0
  119. package/skills/service-omni-service-channel-configure/assets/package.xml +11 -0
  120. package/skills/service-omni-service-channel-configure/assets/sfdx-project.json +12 -0
  121. package/skills/service-omni-service-channel-configure/references/api-notes.md +84 -0
  122. package/skills/service-omni-service-channel-configure/scripts/deploy-and-report.sh +383 -0
  123. package/skills/service-omni-sidebar-configure/SKILL.md +77 -0
  124. package/skills/service-omni-sidebar-configure/references/api-notes.md +51 -0
  125. package/skills/service-omni-sidebar-configure/scripts/enable-and-report.sh +315 -0
  126. package/skills/service-omni-sidebar-configure/scripts/tests/_bootstrap.py +142 -0
  127. package/skills/service-omni-sidebar-configure/scripts/tests/test_sidebar_contracts.py +139 -0
  128. package/skills/service-omni-skills-based-routing-configure/SKILL.md +81 -0
  129. package/skills/service-omni-skills-based-routing-configure/references/api-notes.md +50 -0
  130. package/skills/service-omni-skills-based-routing-configure/scripts/configure-and-report.sh +270 -0
  131. package/skills/service-omni-skills-based-routing-configure/scripts/tests/_bootstrap.py +120 -0
  132. package/skills/service-omni-skills-based-routing-configure/scripts/tests/test_sbr_contracts.py +129 -0
  133. package/skills/service-omni-supervisor-config-deploy/SKILL.md +97 -0
  134. package/skills/service-omni-supervisor-config-deploy/assets/force-app/main/default/omniSupervisorConfigs/Omni_Supervisor.omniSupervisorConfig-meta.xml +9 -0
  135. package/skills/service-omni-supervisor-config-deploy/assets/package.xml +8 -0
  136. package/skills/service-omni-supervisor-config-deploy/assets/sfdx-project.json +12 -0
  137. package/skills/service-omni-supervisor-config-deploy/references/api-notes.md +205 -0
  138. package/skills/service-omni-supervisor-config-deploy/scripts/deploy-and-report.sh +586 -0
  139. package/skills/service-omni-supervisor-config-deploy/scripts/tests/_bootstrap.py +153 -0
  140. package/skills/service-omni-supervisor-config-deploy/scripts/tests/test_supervisor_config_contracts.py +116 -0
  141. package/skills/service-omni-supervisor-permset-assign/SKILL.md +88 -0
  142. package/skills/service-omni-supervisor-permset-assign/references/api-notes.md +48 -0
  143. package/skills/service-omni-supervisor-permset-assign/scripts/verify-and-assign.sh +316 -0
  144. package/skills/service-omni-supervisor-surface-deploy/SKILL.md +85 -0
  145. package/skills/service-omni-supervisor-surface-deploy/references/api-notes.md +47 -0
  146. package/skills/service-omni-supervisor-surface-deploy/scripts/deploy-and-report.sh +216 -0
  147. package/skills/service-omni-supervisor-surface-deploy/scripts/tests/_bootstrap.py +95 -0
  148. package/skills/service-omni-supervisor-surface-deploy/scripts/tests/test_supervisor_surface_contracts.py +154 -0
  149. package/skills/service-omni-supervisor-users-create/SKILL.md +92 -0
  150. package/skills/service-omni-supervisor-users-create/assets/create-supervisors.apex.template +72 -0
  151. package/skills/service-omni-supervisor-users-create/references/apex-patterns.md +73 -0
  152. package/skills/service-omni-supervisor-users-create/references/apex-template-notes.md +41 -0
  153. package/skills/service-omni-supervisor-users-create/scripts/detect-and-create.sh +197 -0
  154. package/skills/service-omni-supervisor-users-create/scripts/detect-existing.sh +112 -0
  155. package/skills/service-omni-supervisor-users-create/scripts/run-create.sh +296 -0
  156. package/skills/service-omni-work-skill-routing-configure/SKILL.md +80 -0
  157. package/skills/service-omni-work-skill-routing-configure/references/api-notes.md +67 -0
  158. package/skills/service-omni-work-skill-routing-configure/scripts/configure-and-report.sh +262 -0
  159. package/skills/service-omni-work-skill-routing-configure/scripts/tests/_bootstrap.py +121 -0
  160. package/skills/service-omni-work-skill-routing-configure/scripts/tests/test_work_skill_routing_contracts.py +161 -0
@@ -0,0 +1,77 @@
1
+ ---
2
+ name: service-omni-sidebar-configure
3
+ description: "Use to enable or verify the Omni-Channel sidebar on a Lightning console app. Triggers: enable the Omni sidebar, pin Omni-Channel in a console app, configure the Omni sidebar, turn on the Omni-Channel utility region, verify the pinned Omni widget. Do not use for Aura apps, standard apps, utility-bar-only Omni widgets, or to create a console app."
4
+ allowed-tools: Bash Read Write Edit Glob Grep
5
+ metadata:
6
+ version: "1.0"
7
+ domains: ["Service"]
8
+ minApiVersion: "66.0"
9
+ relatedSkills:
10
+ - "service-omni-base-settings-configure"
11
+ - "service-omni-channel-setup-coordinate"
12
+ accessCheck:
13
+ - type: license
14
+ value: ServiceCloud
15
+ cliTools:
16
+ - tool: ["jq"]
17
+ semver: ">=1.6"
18
+ - tool: ["python3"]
19
+ semver: ">=3.8"
20
+ - tool: ["sf"]
21
+ semver: ">=2.139.6"
22
+ ---
23
+
24
+ # service-omni-sidebar-configure
25
+
26
+ Enable the Omni-Channel **sidebar** — the pinned Omni utility region that docks to the side of a Lightning console app — by setting `isOmniPinnedViewEnabled=true` on that app's `CustomApplication` metadata and confirming the flag round-trips. This is the recommended Omni surface for console/demo orgs: reps see incoming work, presence, and the work list docked beside the record, rather than only in the collapsible utility bar. It runs once per console app and is invoked by `service-omni-channel-setup-coordinate` as the final rep-experience step.
27
+
28
+ ## Inputs
29
+
30
+ ```bash
31
+ bash scripts/enable-and-report.sh <org-alias> [app_developer_name]
32
+ ```
33
+
34
+ - `org-alias` (required).
35
+ - `app_developer_name` (optional). The `CustomApplication` DeveloperName of the Lightning console app. When omitted, the skill queries `AppDefinition` for Lightning console apps: exactly one → adopt it; zero or many → `blocked` (name it explicitly).
36
+
37
+ ## Preconditions and safety
38
+
39
+ - Target org authenticated via `sf` CLI, Service Cloud license, `sf` CLI ≥ 2.139.6.
40
+ - Omni-Channel base settings enabled (`service-omni-base-settings-configure`).
41
+ - The target must be a **Lightning console** app that is deployable as `CustomApplication` metadata. Standard apps and Aura apps are out of scope.
42
+ - The three-way `safe_to_write` production guard applies.
43
+
44
+ ## Run
45
+
46
+ `enable-and-report.sh` resolves the target app, retrieves its `CustomApplication`, and reads the current `isOmniPinnedViewEnabled` value:
47
+
48
+ - already `true` → `reused` (no deploy).
49
+ - `false` or absent → sets it to `true` and deploys the single `CustomApplication` in one atomic Metadata API call, then **re-retrieves** to confirm the flag is `true` before reporting success.
50
+
51
+ The deploy uses explicit `--metadata "CustomApplication:<name>"` (never `--source-dir`) and is done on its own — Metadata deploys are atomic, so the app is left unchanged on any failure.
52
+
53
+ ## Behavior
54
+
55
+ **Idempotent + non-destructive.** The skill only flips the single boolean; it never rewrites tabs, brand, or nav config. When the field is missing it is inserted in its XSD-ordered position (immediately before `<label>`); when present its value is replaced in place.
56
+
57
+ **Auto-detect is conservative.** It adopts an app automatically only when exactly one Lightning console app exists, so it can never silently pin the wrong app on an org with several.
58
+
59
+ ## Output contract
60
+
61
+ A single JSON object: `status` ∈ `enabled` | `reused` | `blocked`, `app_developer_name`, `app_label`, `before` (bool), `after` (bool), `deploy_id`, `manual_actions`, `blocking_issue`.
62
+
63
+ - `enabled` — the flag was `false`/absent and is now verified `true`.
64
+ - `reused` — the flag was already `true`.
65
+ - `blocked` — no/many console apps found, retrieve/deploy failure, or the post-verify did not confirm `true`; `blocking_issue` explains and `manual_actions` names the fix or prerequisite skill.
66
+
67
+ ## Limitations
68
+
69
+ - One app per invocation; run again for each console app that needs the sidebar.
70
+ - Lightning console `CustomApplication` only — not Aura, not standard apps, not the utility-bar Omni widget.
71
+ - Does not create or lay out the console app, its tabs, or the Omni utility item.
72
+
73
+ ## References
74
+
75
+ | File | When to read |
76
+ |---|---|
77
+ | `references/api-notes.md` | CustomApplication `isOmniPinnedViewEnabled` schema/order, AppDefinition detection query, and retrieve/deploy notes |
@@ -0,0 +1,51 @@
1
+ # API notes — service-omni-sidebar-configure
2
+
3
+ ## The setting
4
+
5
+ The Omni-Channel **sidebar** (the pinned Omni utility region docked to the side of a console
6
+ workspace) is controlled by a single boolean on the `CustomApplication` metadata:
7
+
8
+ ```xml
9
+ <isOmniPinnedViewEnabled>true</isOmniPinnedViewEnabled>
10
+ ```
11
+
12
+ - It is a **per-app** setting on a Lightning **console** app — not an org-level `OmniChannelSettings`
13
+ flag. Enabling Omni-Channel org-wide (`service-omni-base-settings-configure`) is a prerequisite,
14
+ but does not pin the sidebar on any app.
15
+ - When absent, the platform treats it as `false`. This skill therefore inserts the element when it
16
+ is missing rather than assuming a default.
17
+
18
+ ## XSD ordering
19
+
20
+ `CustomApplication` enforces element order. The `is*` booleans
21
+ (`isNavAutoTempTabsDisabled`, `isNavPersonalizationDisabled`, `isNavTabPersistenceDisabled`,
22
+ `isOmniPinnedViewEnabled`) precede `<label>`. The skill inserts `<isOmniPinnedViewEnabled>`
23
+ immediately before the first top-level `<label>`, which keeps the payload schema-valid whether or
24
+ not the other booleans are present. If a future release reorders the schema and the deploy fails
25
+ with an XSD-order error, adjust the insertion anchor here.
26
+
27
+ ## Detecting console apps (auto-detect path)
28
+
29
+ When no app name is supplied, candidates come from the Tooling API:
30
+
31
+ ```sql
32
+ SELECT DeveloperName, Label FROM AppDefinition WHERE UiType='Lightning' AND NavType='Console'
33
+ ```
34
+
35
+ `AppDefinition.DeveloperName` matches the `CustomApplication` DeveloperName used for retrieve/deploy
36
+ for custom apps. The skill adopts a target automatically **only** when exactly one row is returned;
37
+ zero or many → `blocked` so it can never pin the wrong app. Standard apps surfaced by this query are
38
+ generally not deployable as `CustomApplication`; if one is named explicitly the retrieve returns no
39
+ `.app-meta.xml` and the skill blocks with a clear message.
40
+
41
+ ## Retrieve / deploy
42
+
43
+ - Retrieve and deploy use explicit `--metadata "CustomApplication:<name>"` (never `--source-dir`) so
44
+ no sibling metadata can piggyback.
45
+ - The deploy runs `--async` and polls `sf project deploy report` to a terminal state — a synchronous
46
+ watched deploy can raise `ClientTimeoutError` under org load even when the server-side deploy
47
+ succeeds (same false-red class handled by the other Omni leaves).
48
+ - Metadata deploys are atomic, so a failed deploy leaves the app unchanged; the skill then reports
49
+ `blocked` and never claims partial success.
50
+ - Success is confirmed by a **re-retrieve** that reads `isOmniPinnedViewEnabled` back as `true`; a
51
+ green deploy that does not round-trip the flag is reported `blocked`, not `enabled`.
@@ -0,0 +1,315 @@
1
+ #!/usr/bin/env bash
2
+ # enable-and-report.sh - enable the Omni-Channel sidebar on a Lightning console app (CustomApplication.isOmniPinnedViewEnabled=true) and verify round-trip; idempotent, non-destructive (only the one boolean). Args, exit codes: SKILL.md.
3
+
4
+ set -euo pipefail
5
+
6
+ if [ $# -lt 1 ]; then
7
+ echo '{"error":"Usage: bash enable-and-report.sh <org-alias> [app_developer_name]"}' >&2
8
+ exit 1
9
+ fi
10
+
11
+ ORG="$1"
12
+ APP_DN_ARG="${2:-}"
13
+
14
+ # App DeveloperName grammar (when supplied): leading letter, then letters/digits/underscore, max 80.
15
+ # A namespace prefix (ns__Name) is permitted for packaged apps.
16
+ if [ -n "$APP_DN_ARG" ] && ! [[ "$APP_DN_ARG" =~ ^([A-Za-z][A-Za-z0-9]{0,14}__)?[A-Za-z][A-Za-z0-9_]{0,79}$ ]]; then
17
+ echo "{\"error\":\"Invalid app_developer_name '$APP_DN_ARG'. Must be a CustomApplication DeveloperName (letters, digits, underscores; optional ns__ prefix).\"}" >&2
18
+ exit 1
19
+ fi
20
+
21
+ if ! sf org display --target-org "$ORG" --json >/dev/null 2>&1; then
22
+ echo "{\"error\":\"Org alias '$ORG' is not authenticated. Run: sf org login web -a $ORG -r <my-domain-url>\"}" >&2
23
+ exit 1
24
+ fi
25
+
26
+ # Helpers
27
+ emit_blocked() {
28
+ local msg="$1"
29
+ local click_path="${2:-}"
30
+ local app_dn="${3:-}"
31
+ local target_skill="${4:-}"
32
+
33
+ local manual_actions='[]'
34
+ if [ -n "$target_skill" ]; then
35
+ manual_actions=$(jq -n --arg ts "$target_skill" '[{id: "MISSING_PREREQ", title: "Run prerequisite skill", target_skill: $ts}]')
36
+ elif [ -n "$click_path" ]; then
37
+ manual_actions=$(jq -n --arg cp "$click_path" '[{id: "MANUAL_FIX", title: "Manual fix required", click_path: $cp}]')
38
+ fi
39
+
40
+ EMITTED=1
41
+ jq -n \
42
+ --arg msg "$msg" \
43
+ --arg dn "$app_dn" \
44
+ --argjson ma "$manual_actions" \
45
+ '{
46
+ skill: "service-omni-sidebar-configure",
47
+ status: "blocked",
48
+ app_developer_name: (if $dn == "" then null else $dn end),
49
+ app_label: null,
50
+ before: null,
51
+ after: null,
52
+ deploy_id: null,
53
+ manual_actions: $ma,
54
+ blocking_issue: $msg
55
+ }'
56
+ exit 1
57
+ }
58
+
59
+ emit_plan() {
60
+ local status="$1"
61
+ local detail="$2"
62
+ local app_dn="$3"
63
+ EMITTED=1
64
+ jq -n \
65
+ --arg status "$status" \
66
+ --arg detail "$detail" \
67
+ --arg dn "$app_dn" \
68
+ '{
69
+ skill: "service-omni-sidebar-configure",
70
+ status: $status,
71
+ plan_mode: true,
72
+ plan_detail: $detail,
73
+ app_developer_name: (if $dn == "" then null else $dn end),
74
+ manual_actions: [],
75
+ blocking_issue: null
76
+ }'
77
+ exit 0
78
+ }
79
+
80
+ # Fail-safe: the leaf runs under `set -euo pipefail` with several unguarded jq/sed/awk calls, so a
81
+ # transient failure (e.g. a flaky Metadata API) could otherwise kill it with empty stdout. Emit a
82
+ # structured blocked JSON on any exit that happens before a real result, so the caller never parses
83
+ # empty output. EMITTED is flipped to 1 immediately before each intentional emission below.
84
+ EMITTED=0
85
+ WORK=""
86
+ cleanup_work() { [ -n "${WORK:-}" ] && rm -rf "$WORK" 2>/dev/null || true; }
87
+ on_exit() {
88
+ cleanup_work
89
+ if [ "$EMITTED" != "1" ]; then
90
+ jq -n --arg dn "${APP_DN:-${APP_DN_ARG:-}}" \
91
+ '{skill:"service-omni-sidebar-configure", status:"blocked",
92
+ app_developer_name:(if $dn=="" then null else $dn end),
93
+ app_label:null, before:null, after:null, deploy_id:null, manual_actions:[],
94
+ blocking_issue:"Sidebar enablement aborted before completing (unexpected error; see stderr). Re-run, optionally with an explicit app_developer_name."}'
95
+ fi
96
+ }
97
+ trap on_exit EXIT
98
+
99
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
100
+
101
+ retrieve_app_xml() {
102
+ # Retrieve CustomApplication:<member> into a fresh temp DX project and echo the .app-meta.xml path
103
+ # (empty if none returned). Caller owns cleanup of the printed dir's parent.
104
+ local member="${1:-$APP_DN}"
105
+ local work; work="$(mktemp -d)"
106
+ cp "$SCRIPT_DIR/../assets/sfdx-project.json" "$work/sfdx-project.json" 2>/dev/null || echo '{"packageDirectories":[{"path":"force-app","default":true}],"sourceApiVersion":"66.0"}' > "$work/sfdx-project.json"
107
+ mkdir -p "$work/force-app/main/default"
108
+ ( cd "$work" && sf project retrieve start --target-org "$ORG" \
109
+ --metadata "CustomApplication:${member}" --json >/dev/null 2>&1 || true )
110
+ local xml; xml=$(find "$work" -name "*.app-meta.xml" 2>/dev/null | head -1)
111
+ echo "$work|$xml"
112
+ }
113
+
114
+ # Step 1 - Organization + safe_to_write guard
115
+ ORG_GUARD_JSON=$(sf data query --target-org "$ORG" \
116
+ --query "SELECT IsSandbox, TrialExpirationDate, OrganizationType FROM Organization LIMIT 1" \
117
+ --json 2>/dev/null) || emit_blocked "Failed to query Organization for the safe_to_write guard."
118
+ IS_SANDBOX=$(echo "$ORG_GUARD_JSON" | jq -r '.result.records[0].IsSandbox')
119
+ TRIAL_EXP=$(echo "$ORG_GUARD_JSON" | jq -r '.result.records[0].TrialExpirationDate // "null"')
120
+ ORG_TYPE=$(echo "$ORG_GUARD_JSON" | jq -r '.result.records[0].OrganizationType')
121
+
122
+ SAFE_TO_WRITE=false
123
+ if [ "$IS_SANDBOX" = "true" ] \
124
+ || [ "$TRIAL_EXP" != "null" ] \
125
+ || [ "$ORG_TYPE" = "Developer Edition" ] \
126
+ || [ "$ORG_TYPE" = "Base Edition" ]; then
127
+ SAFE_TO_WRITE=true
128
+ fi
129
+
130
+ if [ "$SAFE_TO_WRITE" != "true" ]; then
131
+ emit_blocked "Refusing to modify an app on a real production customer org (IsSandbox=$IS_SANDBOX, TrialExpirationDate=$TRIAL_EXP, OrganizationType=$ORG_TYPE). Target a sandbox, trial CDO, or Developer Edition org."
132
+ fi
133
+
134
+ # Step 2 - Resolve the target Lightning console app
135
+ APP_DN="$APP_DN_ARG"
136
+ APP_LABEL=""
137
+ if [ -z "$APP_DN" ]; then
138
+ # Standard apps are frequently read-only even though AppDefinition reports them as console apps.
139
+ # Enumerate actual CustomApplication members and inspect only non-standard metadata so automatic
140
+ # selection cannot pick an undeployable platform app.
141
+ APP_METADATA_JSON=$(sf org list metadata --metadata-type CustomApplication \
142
+ --target-org "$ORG" --json 2>/dev/null) \
143
+ || emit_blocked "Could not list CustomApplication metadata to auto-detect a deployable Lightning console app. Re-run with a reachable org, or pass an explicit app_developer_name."
144
+ echo "$APP_METADATA_JSON" | jq -e '.result | arrays' >/dev/null 2>&1 \
145
+ || emit_blocked "CustomApplication metadata discovery returned no parseable result (inconclusive). Re-run, or pass an explicit app_developer_name."
146
+
147
+ CANDIDATES='[]'
148
+ CUSTOM_MEMBERS=$(echo "$APP_METADATA_JSON" | jq -r '.result[]?.fullName | select(startswith("standard__") | not)')
149
+ for APP_MEMBER in $CUSTOM_MEMBERS; do
150
+ CANDIDATE_RET=$(retrieve_app_xml "$APP_MEMBER")
151
+ CANDIDATE_WORK="${CANDIDATE_RET%%|*}"
152
+ CANDIDATE_XML="${CANDIDATE_RET#*|}"
153
+ if [ -n "$CANDIDATE_XML" ] && [ -f "$CANDIDATE_XML" ] \
154
+ && grep -q '<uiType>Lightning</uiType>' "$CANDIDATE_XML" \
155
+ && grep -q '<navType>Console</navType>' "$CANDIDATE_XML"; then
156
+ CANDIDATES=$(echo "$CANDIDATES" | jq --arg dn "$APP_MEMBER" '. + [$dn]')
157
+ fi
158
+ rm -rf "$CANDIDATE_WORK"
159
+ done
160
+
161
+ APP_COUNT=$(echo "$CANDIDATES" | jq 'length')
162
+ if [ "$APP_COUNT" = "0" ]; then
163
+ emit_blocked "No deployable custom Lightning console app found on this org. Create one first, then pass its DeveloperName." \
164
+ "Setup → App Manager → New Lightning App → Navigation Style: Console → Save"
165
+ elif [ "$APP_COUNT" != "1" ]; then
166
+ emit_blocked "Found $APP_COUNT deployable custom Lightning console apps ($CANDIDATES). Re-run with an explicit app_developer_name so the sidebar is enabled on the intended app."
167
+ fi
168
+ APP_DN=$(echo "$CANDIDATES" | jq -r '.[0]')
169
+ fi
170
+
171
+ # Step 3 - Retrieve the app's CustomApplication metadata
172
+ RET=$(retrieve_app_xml "$APP_DN")
173
+ WORK="${RET%%|*}"
174
+ APP_XML="${RET#*|}"
175
+
176
+ # Standard apps expose AppDefinition.DeveloperName without the standard__ prefix, but the
177
+ # CustomApplication metadata member requires it (e.g. standard__LightningService). If the plain
178
+ # member did not resolve and the name is unprefixed, retry with standard__ and adopt whichever
179
+ # member actually retrieves, so the later deploy targets the correct component.
180
+ if { [ -z "$APP_XML" ] || [ ! -f "$APP_XML" ]; } && [[ "$APP_DN" != *"__"* ]]; then
181
+ cleanup_work
182
+ RET=$(retrieve_app_xml "standard__$APP_DN")
183
+ WORK="${RET%%|*}"
184
+ APP_XML="${RET#*|}"
185
+ { [ -n "$APP_XML" ] && [ -f "$APP_XML" ]; } && APP_DN="standard__$APP_DN"
186
+ fi
187
+
188
+ if [ -z "$APP_XML" ] || [ ! -f "$APP_XML" ]; then
189
+ emit_blocked "Could not retrieve CustomApplication '$APP_DN' (it may not exist, may be a standard/Aura app that is not deployable as CustomApplication, or the retrieve returned no metadata)." \
190
+ "Setup → App Manager → confirm '$APP_DN' is a custom Lightning console app"
191
+ fi
192
+
193
+ # Current value of the boolean (true only when the element is present AND set true).
194
+ current_flag() {
195
+ if grep -oE "<isOmniPinnedViewEnabled>[^<]*</isOmniPinnedViewEnabled>" "$1" 2>/dev/null \
196
+ | grep -qi ">true<"; then
197
+ echo true
198
+ else
199
+ echo false
200
+ fi
201
+ }
202
+ BEFORE=$(current_flag "$APP_XML")
203
+
204
+ # PLAN_ONLY - all reads above are read-only; report intent and stop before any write.
205
+ if [ "${PLAN_ONLY:-}" = "1" ]; then
206
+ if [ "$BEFORE" = "true" ]; then
207
+ emit_plan "reused" "Omni sidebar already enabled (isOmniPinnedViewEnabled=true) on '$APP_DN'." "$APP_DN"
208
+ else
209
+ emit_plan "action_needed" "Would set isOmniPinnedViewEnabled=true on Lightning console app '$APP_DN'." "$APP_DN"
210
+ fi
211
+ fi
212
+
213
+ # Step 4 - Already enabled → reused (no deploy)
214
+ if [ "$BEFORE" = "true" ]; then
215
+ EMITTED=1
216
+ jq -n --arg dn "$APP_DN" --arg lbl "$APP_LABEL" \
217
+ '{
218
+ skill: "service-omni-sidebar-configure",
219
+ status: "reused",
220
+ app_developer_name: $dn,
221
+ app_label: (if $lbl == "" then null else $lbl end),
222
+ before: true,
223
+ after: true,
224
+ deploy_id: null,
225
+ manual_actions: [],
226
+ blocking_issue: null
227
+ }'
228
+ exit 0
229
+ fi
230
+
231
+ # Step 5 - Set the flag (replace in place, or insert in XSD order before <label>)
232
+ if grep -q "<isOmniPinnedViewEnabled>" "$APP_XML"; then
233
+ sed -E "s|<isOmniPinnedViewEnabled>[^<]*</isOmniPinnedViewEnabled>|<isOmniPinnedViewEnabled>true</isOmniPinnedViewEnabled>|" \
234
+ "$APP_XML" > "$APP_XML.tmp" && mv "$APP_XML.tmp" "$APP_XML"
235
+ else
236
+ # Insert immediately before the first top-level <label> - its XSD-ordered position in
237
+ # CustomApplication (the is* booleans precede <label>). Only the first match is edited.
238
+ awk 'BEGIN{done=0}
239
+ /<label>/ && !done { print " <isOmniPinnedViewEnabled>true</isOmniPinnedViewEnabled>"; done=1 }
240
+ { print }' "$APP_XML" > "$APP_XML.tmp" && mv "$APP_XML.tmp" "$APP_XML"
241
+ fi
242
+
243
+ if [ "$(current_flag "$APP_XML")" != "true" ]; then
244
+ emit_blocked "Failed to set isOmniPinnedViewEnabled in the retrieved CustomApplication payload for '$APP_DN' (unexpected app metadata shape)." "" "$APP_DN"
245
+ fi
246
+
247
+ # Step 6 - Deploy the single CustomApplication (async + poll to a terminal state)
248
+ START_JSON=$(cd "$WORK" && sf project deploy start --target-org "$ORG" \
249
+ --metadata "CustomApplication:${APP_DN}" --async --json 2>/dev/null || true)
250
+ DEPLOY_ID=$(echo "$START_JSON" | jq -r '.result.id // ""' 2>/dev/null)
251
+ DEPLOY_JSON="$START_JSON"
252
+
253
+ if [ -n "$DEPLOY_ID" ] && [ "$DEPLOY_ID" != "null" ]; then
254
+ POLL_STATUS=""
255
+ for _ in $(seq 1 40); do
256
+ REPORT_JSON=$(cd "$WORK" && sf project deploy report --job-id "$DEPLOY_ID" --target-org "$ORG" --json 2>/dev/null || true)
257
+ CUR_STATUS=$(echo "$REPORT_JSON" | jq -r '.result.status // ""' 2>/dev/null)
258
+ case "$CUR_STATUS" in
259
+ Succeeded|Failed|SucceededPartial|Canceled) DEPLOY_JSON="$REPORT_JSON"; POLL_STATUS="$CUR_STATUS"; break ;;
260
+ esac
261
+ sleep 15
262
+ done
263
+ if [ -z "$POLL_STATUS" ]; then
264
+ emit_blocked "CustomApplication deploy job $DEPLOY_ID for '$APP_DN' did not reach a terminal state within the poll budget. Not reporting success." "" "$APP_DN"
265
+ fi
266
+ fi
267
+
268
+ DEPLOY_SUCCESS=$(echo "$DEPLOY_JSON" | jq -r '.result.success // false' 2>/dev/null)
269
+ DEPLOY_ID=$(echo "$DEPLOY_JSON" | jq -r '.result.id // ""' 2>/dev/null)
270
+
271
+ if [ "$DEPLOY_SUCCESS" != "true" ]; then
272
+ FAILURES=$(echo "$DEPLOY_JSON" | jq -r '.result.details.componentFailures // [] | map(.problem) | join("; ")' 2>/dev/null)
273
+ if [ -z "$FAILURES" ] || [ "$FAILURES" = "null" ]; then
274
+ FAILURES=$(echo "$DEPLOY_JSON" | jq -r '.message // .result.errorMessage // "Unknown deploy error"' 2>/dev/null)
275
+ fi
276
+ # Standard console apps (standard__*) are frequently read-only via the Metadata API, so the
277
+ # fallback deploy can be rejected. Give a targeted remediation instead of only the raw error.
278
+ if [[ "$APP_DN" == standard__* ]]; then
279
+ emit_blocked "Deploy of the standard console app '$APP_DN' failed: $FAILURES. Standard apps are often not modifiable via the Metadata API - enable the Omni sidebar in Setup, or re-run this skill against a custom Lightning console app (pass its DeveloperName)." \
280
+ "Setup → App Manager → ${APP_DN#standard__} → Edit → enable the Omni-Channel pinned/utility sidebar" "$APP_DN"
281
+ fi
282
+ emit_blocked "Deploy of CustomApplication '$APP_DN' failed: $FAILURES" "" "$APP_DN"
283
+ fi
284
+
285
+ # Step 7 - Post-verify: re-retrieve and confirm the flag is true on the org
286
+ AFTER="unverified"
287
+ for verify_attempt in 1 2 3; do
288
+ VRET=$(retrieve_app_xml "$APP_DN")
289
+ VWORK="${VRET%%|*}"; VXML="${VRET#*|}"
290
+ if [ -n "$VXML" ] && [ -f "$VXML" ]; then
291
+ AFTER=$(current_flag "$VXML")
292
+ rm -rf "$VWORK"
293
+ break
294
+ fi
295
+ rm -rf "$VWORK"
296
+ [ "$verify_attempt" -lt 3 ] && sleep 10
297
+ done
298
+
299
+ if [ "$AFTER" != "true" ]; then
300
+ emit_blocked "Deploy reported success but isOmniPinnedViewEnabled is not true for '$APP_DN' on post-verify (retrieved metadata does not confirm the sidebar is pinned)." "" "$APP_DN"
301
+ fi
302
+
303
+ EMITTED=1
304
+ jq -n --arg dn "$APP_DN" --arg lbl "$APP_LABEL" --arg deploy_id "$DEPLOY_ID" \
305
+ '{
306
+ skill: "service-omni-sidebar-configure",
307
+ status: "enabled",
308
+ app_developer_name: $dn,
309
+ app_label: (if $lbl == "" then null else $lbl end),
310
+ before: false,
311
+ after: true,
312
+ deploy_id: (if $deploy_id == "" then null else $deploy_id end),
313
+ manual_actions: [],
314
+ blocking_issue: null
315
+ }'
@@ -0,0 +1,142 @@
1
+ """Shared paths + fake-`sf` harness for the sidebar-configure contract suite (org-free, network-free): drives enable-and-report.sh via subprocess with scrubbed-PATH and production/sandbox fake-sf fixtures (un-pinned/pinned app, console-app auto-detect cardinality)."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import subprocess
6
+ import tempfile
7
+ from pathlib import Path
8
+
9
+ SKILLS_ROOT = Path(__file__).resolve().parents[3]
10
+ SKILL_DIR = "service-omni-sidebar-configure"
11
+ SCRIPT = f"{SKILL_DIR}/scripts/enable-and-report.sh"
12
+
13
+ # PRODUCTION org - the safe_to_write guard must refuse before any write.
14
+ _FAKE_SF_PROD = r"""#!/usr/bin/env bash
15
+ sub="${1:-} ${2:-}"; args="$*"
16
+ case "$sub" in
17
+ "org display") echo '{"status":0,"result":{"id":"00Dxx0000000000EAA"}}'; exit 0;;
18
+ "data query")
19
+ if printf '%s' "$args" | grep -q "FROM Organization"; then
20
+ echo '{"status":0,"result":{"records":[{"IsSandbox":false,"TrialExpirationDate":null,"OrganizationType":"Enterprise Edition"}]}}'
21
+ else
22
+ echo '{"status":0,"result":{"records":[],"totalSize":0}}'
23
+ fi
24
+ exit 0;;
25
+ "project deploy") echo '{"status":1,"result":{"success":false},"message":"FAKE_SF_DEPLOY_SHOULD_NOT_BE_REACHED"}'; exit 1;;
26
+ *) echo '{"status":0,"result":{}}'; exit 0;;
27
+ esac
28
+ """
29
+
30
+ # SANDBOX org. Tunables:
31
+ # FAKE_APP_PINNED=1 → the retrieved CustomApplication already has isOmniPinnedViewEnabled=true
32
+ # (reused path). Default 0 → the element is absent (insert-before-label path).
33
+ # FAKE_CONSOLE_APPS=N → number of deployable custom Lightning console apps returned by metadata
34
+ # discovery (0 → blocked, 1 → adopt, >1 → blocked). Default 1.
35
+ # A successful deploy touches a marker next to the fake sf; the subsequent re-retrieve then reports
36
+ # the flag as true, so the enable → deploy → post-verify path converges statefully.
37
+ _FAKE_SF_SANDBOX = r"""#!/usr/bin/env bash
38
+ sub="${1:-} ${2:-}"; args="$*"
39
+ SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
40
+ APP_PINNED="${FAKE_APP_PINNED:-0}"
41
+ CONSOLE_APPS="${FAKE_CONSOLE_APPS:-1}"
42
+ if [ -n "${FAKE_SF_CALLLOG:-}" ]; then printf '%s\n' "$args" >> "$FAKE_SF_CALLLOG"; fi
43
+ case "$sub" in
44
+ "org display") echo '{"status":0,"result":{"id":"00Dxx0000000000EAA"}}'; exit 0;;
45
+ "org list")
46
+ case "$CONSOLE_APPS" in
47
+ 0) echo '{"status":0,"result":[{"fullName":"standard__LightningService"}]}' ;;
48
+ 1) echo '{"status":0,"result":[{"fullName":"standard__LightningService"},{"fullName":"Demo_Console"}]}' ;;
49
+ *) echo '{"status":0,"result":[{"fullName":"standard__LightningService"},{"fullName":"Demo_Console"},{"fullName":"Other_Console"}]}' ;;
50
+ esac
51
+ exit 0;;
52
+ "data query")
53
+ if printf '%s' "$args" | grep -q "FROM Organization"; then
54
+ echo '{"status":0,"result":{"records":[{"IsSandbox":true,"TrialExpirationDate":null,"OrganizationType":"Developer Edition"}]}}'
55
+ elif printf '%s' "$args" | grep -q "FROM AppDefinition"; then
56
+ if printf '%s' "$args" | grep -q -- "--use-tooling-api"; then
57
+ echo '{"status":1,"name":"INVALID_TYPE","message":"AppDefinition is not supported by Tooling API"}'
58
+ exit 1
59
+ fi
60
+ case "$CONSOLE_APPS" in
61
+ 0) echo '{"status":0,"result":{"records":[],"totalSize":0}}';;
62
+ 1) echo '{"status":0,"result":{"records":[{"DeveloperName":"Demo_Console","Label":"Demo Console"}],"totalSize":1}}';;
63
+ *) echo '{"status":0,"result":{"records":[{"DeveloperName":"Demo_Console","Label":"Demo Console"},{"DeveloperName":"Other_Console","Label":"Other Console"}],"totalSize":2}}';;
64
+ esac
65
+ else
66
+ echo '{"status":0,"result":{"records":[],"totalSize":0}}'
67
+ fi
68
+ exit 0;;
69
+ "project retrieve")
70
+ appname=$(printf '%s' "$args" | sed -n 's/.*CustomApplication:\([A-Za-z0-9_]*\).*/\1/p'); appname="${appname:-Demo_Console}"
71
+ pinned=false
72
+ if [ "$APP_PINNED" = "1" ] || [ -f "$SELF_DIR/.deployed" ]; then pinned=true; fi
73
+ if [ "$pinned" = "true" ]; then
74
+ cat > "./${appname}.app-meta.xml" <<XML
75
+ <?xml version="1.0" encoding="UTF-8"?>
76
+ <CustomApplication xmlns="http://soap.sforce.com/2006/04/metadata">
77
+ <isOmniPinnedViewEnabled>true</isOmniPinnedViewEnabled>
78
+ <label>${appname}</label>
79
+ <navType>Console</navType>
80
+ <uiType>Lightning</uiType>
81
+ </CustomApplication>
82
+ XML
83
+ else
84
+ cat > "./${appname}.app-meta.xml" <<XML
85
+ <?xml version="1.0" encoding="UTF-8"?>
86
+ <CustomApplication xmlns="http://soap.sforce.com/2006/04/metadata">
87
+ <label>${appname}</label>
88
+ <navType>Console</navType>
89
+ <uiType>Lightning</uiType>
90
+ </CustomApplication>
91
+ XML
92
+ fi
93
+ echo '{"status":0,"result":{"success":true}}'; exit 0;;
94
+ "project deploy")
95
+ if printf '%s' "$args" | grep -q "report"; then
96
+ echo '{"status":0,"result":{"success":true,"status":"Succeeded","id":"0AfXX0000000001"}}'
97
+ else
98
+ touch "$SELF_DIR/.deployed"
99
+ echo '{"status":0,"result":{"success":true,"status":"Succeeded","id":"0AfXX0000000001"}}'
100
+ fi
101
+ exit 0;;
102
+ *) echo '{"status":0,"result":{}}'; exit 0;;
103
+ esac
104
+ """
105
+
106
+
107
+ def run(args, path_prefix=None, scrub_sf=False, extra_env=None):
108
+ """Run the orchestrator; return (returncode, merged stdout+stderr)."""
109
+ env = dict(os.environ)
110
+ if scrub_sf:
111
+ kept = [d for d in env.get("PATH", "").split(os.pathsep)
112
+ if d and not os.path.exists(os.path.join(d, "sf"))]
113
+ env["PATH"] = os.pathsep.join(kept)
114
+ if path_prefix:
115
+ env["PATH"] = path_prefix + os.pathsep + env.get("PATH", "")
116
+ if extra_env:
117
+ env.update({k: str(v) for k, v in extra_env.items()})
118
+ proc = subprocess.run(
119
+ ["bash", str(SKILLS_ROOT / SCRIPT), *args],
120
+ capture_output=True, text=True, env=env, cwd=str(SKILLS_ROOT),
121
+ )
122
+ return proc.returncode, (proc.stdout or "") + (proc.stderr or "")
123
+
124
+
125
+ class FakeSf:
126
+ """Context manager: a temp dir holding an executable fake `sf` first on PATH."""
127
+
128
+ def __init__(self, body=_FAKE_SF_PROD):
129
+ self._body = body
130
+ self._tmp = None
131
+
132
+ def __enter__(self):
133
+ self._tmp = tempfile.mkdtemp(prefix="sidebar-fake-sf-")
134
+ sf = Path(self._tmp) / "sf"
135
+ sf.write_text(self._body)
136
+ sf.chmod(0o755)
137
+ return self._tmp
138
+
139
+ def __exit__(self, *exc):
140
+ import shutil
141
+ if self._tmp:
142
+ shutil.rmtree(self._tmp, ignore_errors=True)