@salesforce/afv-skills 1.40.0 → 1.41.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 (136) hide show
  1. package/package.json +1 -1
  2. package/skills/automation-flow-generate/SKILL.md +32 -43
  3. package/skills/experience-portal-create/SKILL.md +497 -0
  4. package/skills/experience-portal-create/assets/report-template.md +30 -0
  5. package/skills/experience-portal-create/references/mcp-invocation.md +288 -0
  6. package/skills/experience-portal-create/references/post-creation-activate-publish.md +165 -0
  7. package/skills/experience-portal-create/references/templates.md +253 -0
  8. package/skills/experience-ui-bundle-features-generate/SKILL.md +5 -1
  9. package/skills/experience-ui-bundle-frontend-generate/SKILL.md +2 -0
  10. package/skills/experience-ui-bundle-frontend-generate/references/page.md +1 -0
  11. package/skills/platform-datamask-run/SKILL.md +345 -0
  12. package/skills/platform-datamask-run/references/api-surface.md +130 -0
  13. package/skills/platform-datamask-run/references/policy-authoring.md +185 -0
  14. package/skills/platform-datamask-run/references/run-and-abort.md +116 -0
  15. package/skills/platform-datamask-run/scripts/poll-job.sh +115 -0
  16. package/skills/platform-dataspace-access-configure/SKILL.md +51 -3
  17. package/skills/platform-dataspace-access-configure/scripts/inspect-dataspace-scopes.sh +56 -0
  18. package/skills/platform-lightning-type-widget-coordinate/references/build-plan-format.md +1 -0
  19. package/skills/platform-sandbox-configure/SKILL.md +17 -2
  20. package/skills/platform-trial-org-create/SKILL.md +175 -0
  21. package/skills/platform-trial-org-create/examples/create_request.json +9 -0
  22. package/skills/platform-trial-org-create/examples/error_response.json +41 -0
  23. package/skills/platform-trial-org-create/examples/success_response.json +27 -0
  24. package/skills/platform-trial-org-create/references/error_codes.md +42 -0
  25. package/skills/platform-trial-org-create/references/signup_request_fields.md +69 -0
  26. package/skills/platform-trial-org-create/scripts/create_signup_request.sh +175 -0
  27. package/skills/platform-trial-org-create/scripts/get_signup_request.sh +155 -0
  28. package/skills/platform-widget-generate/SKILL.md +47 -6
  29. package/skills/platform-widget-generate/examples/conditional.json +3 -3
  30. package/skills/platform-widget-generate/examples/list-with-foreach.json +2 -2
  31. package/skills/platform-widget-generate/examples/single-object.json +2 -2
  32. package/skills/platform-widget-generate/references/widget-bundle-layout.md +1 -1
  33. package/skills/service-agentforce-channel-configure/SKILL.md +271 -0
  34. package/skills/service-agentforce-channel-configure/references/agent-wiring.md +97 -0
  35. package/skills/service-agentforce-channel-configure/references/channel-branch-email.md +145 -0
  36. package/skills/service-agentforce-channel-configure/references/channel-branch-voice.md +69 -0
  37. package/skills/service-agentforce-channel-configure/references/channel-types.md +61 -0
  38. package/skills/service-agentforce-channel-configure/references/live-traffic-gate.md +86 -0
  39. package/skills/service-agentforce-channel-configure/references/queue-resolution.md +135 -0
  40. package/skills/service-agentforce-channel-configure/references/routing-flow.md +384 -0
  41. package/skills/service-catalog-template-deploy/SKILL.md +310 -0
  42. package/skills/service-catalog-template-deploy/references/cli-invocation.md +258 -0
  43. package/skills/service-catalog-template-deploy/scripts/activate-verify.mjs +164 -0
  44. package/skills/service-catalog-template-deploy/scripts/build-deploy-payload.mjs +94 -0
  45. package/skills/service-catalog-template-deploy/scripts/resolve-template.mjs +331 -0
  46. package/skills/service-catalog-template-search/SKILL.md +212 -0
  47. package/skills/service-catalog-template-search/references/cli-invocation.md +128 -0
  48. package/skills/service-catalog-template-search/scripts/classify-catalog.mjs +205 -0
  49. package/skills/service-concierge-portal-generate/SKILL.md +126 -0
  50. package/skills/service-concierge-portal-generate/references/portal-deploy-runbook.md +1428 -0
  51. package/skills/service-digital-engagement-channel-configure/SKILL.md +46 -6
  52. package/skills/service-digital-engagement-channel-configure/assets/messaging_channel_template.xml +2 -1
  53. package/skills/service-digital-engagement-channel-configure/examples/asa_agent_channel.xml +4 -1
  54. package/skills/service-helpagent-coordinate/README.md +8 -2
  55. package/skills/service-helpagent-coordinate/SKILL.md +126 -130
  56. package/skills/service-helpagent-coordinate/assets/help-agent-spec.md +70 -53
  57. package/skills/service-helpagent-coordinate/references/agent-script.md +571 -457
  58. package/skills/service-helpagent-coordinate/references/channel-voice.md +38 -9
  59. package/skills/service-helpagent-coordinate/references/channel-web-chat.md +173 -49
  60. package/skills/service-helpagent-coordinate/references/output-report-format.md +126 -0
  61. package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +153 -0
  62. package/skills/service-itsm-agentic-setup-agentforce-coordinate/examples/output-templates.md +79 -0
  63. package/skills/service-itsm-agentic-setup-agentforce-coordinate/scripts/verify-child-verdict.mjs +35 -0
  64. package/skills/service-itsm-agentic-setup-agentforce-studio-configure/SKILL.md +271 -0
  65. package/skills/service-itsm-agentic-setup-agentforce-studio-configure/references/cli-invocation.md +265 -0
  66. package/skills/service-itsm-agentic-setup-agentforce-studio-configure/scripts/classify-enable-plan.mjs +220 -0
  67. package/skills/service-itsm-agentic-setup-agentforce-studio-configure/scripts/classify-final-report.mjs +102 -0
  68. package/skills/service-itsm-agentic-setup-agentforce-studio-configure/scripts/record-enable-result.mjs +73 -0
  69. package/skills/service-itsm-agentic-setup-agentforce-studio-validate/SKILL.md +206 -0
  70. package/skills/service-itsm-agentic-setup-agentforce-studio-validate/references/cli-invocation.md +194 -0
  71. package/skills/service-itsm-agentic-setup-agentforce-studio-validate/scripts/classify-readiness.mjs +223 -0
  72. package/skills/service-itsm-agentic-setup-cmdb-configure/SKILL.md +45 -7
  73. package/skills/service-itsm-agentic-setup-cmdb-configure/references/mcp-invocation.md +45 -5
  74. package/skills/service-itsm-agentic-setup-configure/SKILL.md +116 -0
  75. package/skills/service-itsm-agentic-setup-configure/examples/output-templates.md +64 -0
  76. package/skills/service-itsm-agentic-setup-employee-agent-configure/SKILL.md +158 -0
  77. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/cli-invocation.md +361 -0
  78. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/error-taxonomy.md +44 -0
  79. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/reactivation.md +66 -0
  80. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/report-format.md +66 -0
  81. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/specialized-templates.md +148 -0
  82. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/workflow-detail.md +169 -0
  83. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/build-create-body.mjs +116 -0
  84. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/classify-agent-existence.mjs +185 -0
  85. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/classify-preflight.mjs +168 -0
  86. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/create-scratch-dir.mjs +58 -0
  87. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/render-report.mjs +197 -0
  88. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/SKILL.md +186 -0
  89. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/action-availability.md +51 -0
  90. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/cli-invocation.md +345 -0
  91. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/error-taxonomy.md +44 -0
  92. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/reactivation.md +63 -0
  93. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/report-format.md +44 -0
  94. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/workflow-detail.md +149 -0
  95. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/build-create-body.mjs +110 -0
  96. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-action-availability.mjs +201 -0
  97. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-activate-result.mjs +135 -0
  98. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-agent-existence.mjs +194 -0
  99. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-preflight.mjs +158 -0
  100. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/create-scratch-dir.mjs +58 -0
  101. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/render-report.mjs +191 -0
  102. package/skills/service-itsm-agentic-setup-incident-management/SKILL.md +133 -0
  103. package/skills/service-itsm-agentic-setup-incident-management/examples/output-templates.md +71 -0
  104. package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +308 -0
  105. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone.json +23 -0
  106. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/milestone-patterns.md +193 -0
  107. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/output-templates.md +57 -0
  108. package/skills/service-itsm-agentic-setup-incident-sla-configure/references/mcp-invocation.md +394 -0
  109. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/SKILL.md +266 -0
  110. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/references/cli-invocation.md +106 -0
  111. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/references/helper-contracts.md +142 -0
  112. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/references/permset-topology.md +82 -0
  113. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/scripts/classify-action-surface.mjs +137 -0
  114. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/scripts/classify-assignment-state.mjs +99 -0
  115. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/scripts/classify-permset-availability.mjs +120 -0
  116. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/scripts/resolve-target-user.mjs +86 -0
  117. package/skills/service-itsm-agentic-setup-uel-user-create/SKILL.md +284 -0
  118. package/skills/service-itsm-agentic-setup-uel-user-create/references/mcp-invocation.md +302 -0
  119. package/skills/service-itsm-channels-coordinate/SKILL.md +472 -0
  120. package/skills/service-itsm-incident-mgmt-configure/SKILL.md +212 -0
  121. package/skills/service-itsm-incident-mgmt-configure/references/mcp-invocation.md +225 -0
  122. package/skills/service-itsm-incident-priority-configure/SKILL.md +53 -12
  123. package/skills/service-itsm-swarming-configure/SKILL.md +212 -0
  124. package/skills/service-itsm-teams-configure/SKILL.md +395 -0
  125. package/skills/service-itsm-teams-configure/references/azure-credential-population.md +213 -0
  126. package/skills/service-itsm-teams-configure/references/gotchas.md +23 -0
  127. package/skills/service-itsm-teams-coordinate/SKILL.md +175 -0
  128. package/skills/service-itsm-teams-coordinate/examples/output-templates.md +85 -0
  129. package/skills/service-itsm-teams-debug/SKILL.md +144 -0
  130. package/skills/service-itsm-teams-debug/references/configuration-checklists.md +277 -0
  131. package/skills/service-itsm-teams-debug/references/report-generation.md +95 -0
  132. package/skills/service-itsm-teams-employee-agent-configure/SKILL.md +139 -0
  133. package/skills/service-itsm-teams-employee-agent-configure/assets/Teams_AgentForce.EmbeddedServiceConfig-meta.xml +43 -0
  134. package/skills/service-itsm-teams-employee-agent-configure/references/teams-embedded-employee-agent.md +480 -0
  135. package/skills/service-itsm-teams-itdesk-configure/SKILL.md +232 -0
  136. package/skills/service-itsm-teams-itservice-configure/SKILL.md +391 -0
@@ -0,0 +1,265 @@
1
+ # CLI invocation reference — service-itsm-agentic-setup-agentforce-studio-configure
2
+
3
+ This skill uses the **Salesforce CLI (`sf`)** as its only transport. It is
4
+ **write-capable**: it reads the *Agentforce for IT Service* Salesforce Go
5
+ feature toggles via Connect API (`sf api request rest`), computes a
6
+ dependency-ordered enable plan with a Node helper script, and — after explicit
7
+ user confirmation — enables each disabled toggle via the same Connect API
8
+ route. It extracts no access tokens.
9
+
10
+ ## Why `sf api request rest`, never curl + token
11
+
12
+ `sf api request rest` authenticates using the CLI's stored session for the
13
+ `--target-org` alias — the CLI mints/refreshes the token internally and never
14
+ exposes it. **Do not** do:
15
+
16
+ <!-- skill-validate: ignore -->
17
+ ```bash
18
+ # FORBIDDEN — leaks a bearer token into shell context, bypasses the CLI session
19
+ TOKEN=$(sf org display --json -o <alias> | jq -r '.result.accessToken')
20
+ curl -X POST -H "Authorization: Bearer $TOKEN" \
21
+ https://.../connect/setup/discovery/feature/sales-cloud-agent-studio/enable
22
+ ```
23
+
24
+ Every call this skill makes — read and write — is a plain
25
+ `/services/data/v67.0/connect/...` Connect API path, exactly what
26
+ `sf api request rest` proxies. There is no reason to fall back to curl.
27
+
28
+ ## The feature toggles and dependency order
29
+
30
+ | UI toggle | featureApiName | Depends on |
31
+ |-----------|----------------|------------|
32
+ | Einstein Generative AI | `sales-cloud-einstein-generative-ai` | none |
33
+ | Turn on Agentforce Studio | `sales-cloud-agent-studio` | Einstein GenAI |
34
+ | Agentforce for IT Service (parent umbrella) | `service-cloud-agentforce-for-itsm` | Studio, Einstein GenAI |
35
+ | IT Service Fulfiller (template) | `service-cloud-it-fulfiller-agent` | parent umbrella |
36
+ | IT Service Employee (template) | `service-cloud-requestor-agent` | parent umbrella |
37
+ | Specialized Agent Templates for Employee | `service-cloud-it-service-employee-agent` | parent umbrella |
38
+
39
+ Enable order: Einstein GenAI → Studio → parent umbrella → path-specific
40
+ template(s). The read response echoes unmet dependencies in
41
+ `dependencyStatuses[]`; the classifier encodes this same order in `ORDER` so
42
+ the enable loop never attempts a child before its parent.
43
+
44
+ ## Target org, API version, and the `--json` rule
45
+
46
+ - **Target org**: always `--target-org <alias>` (or `-o <alias>`).
47
+ - **API version**: pinned in the URL path (`/services/data/v67.0/...`). Do not
48
+ hand-edit below `metadata.minApiVersion` (`67.0`).
49
+ - **`--json`**: do **not** add `--json` to `sf api request rest` — it is
50
+ unsupported on some Connect endpoints and errors; the raw stdout body is
51
+ already JSON.
52
+
53
+ ## Read — feature status
54
+
55
+ ```bash
56
+ sf api request rest "/services/data/v67.0/connect/setup/discovery/features/status" \
57
+ --method POST \
58
+ --body '{"featureApiNames":["sales-cloud-einstein-generative-ai","sales-cloud-agent-studio","service-cloud-agentforce-for-itsm","service-cloud-it-fulfiller-agent","service-cloud-requestor-agent","service-cloud-it-service-employee-agent"]}' \
59
+ --target-org <alias> > /tmp/enable-status-before.json 2>/tmp/enable-status-before.err
60
+ echo $? > /tmp/enable-status-before.exit
61
+ ```
62
+
63
+ Response shape (abridged):
64
+
65
+ ```json
66
+ {
67
+ "items": [
68
+ { "apiName": "sales-cloud-einstein-generative-ai", "status": "ENABLED", "enableBlockedReasons": [], "dependencyStatuses": [] },
69
+ { "apiName": "sales-cloud-agent-studio", "status": "NOT_ENABLED", "enableBlockedReasons": [], "dependencyStatuses": [{ "apiName": "sales-cloud-einstein-generative-ai", "status": "ENABLED" }] }
70
+ ]
71
+ }
72
+ ```
73
+
74
+ - `status: "ENABLED"` ⇒ already on ⇒ skip the write (idempotent).
75
+ - `status: "NOT_ENABLED"` with empty `enableBlockedReasons` ⇒ enable-able.
76
+ - `status: "NOT_ENABLED"` with non-empty `enableBlockedReasons` ⇒ **do not
77
+ attempt to enable** — report the blocker.
78
+ - A confirmed `404`/NOT_FOUND error body ⇒ whole read is CANNOT-CONFIRM (gate
79
+ not wired).
80
+ - An empty body (CLI exited non-zero) or an auth/permission/unexpected error
81
+ body ⇒ whole read is ERROR — surface `rawError` and stop.
82
+
83
+ ## Write — enable a feature
84
+
85
+ ```bash
86
+ sf api request rest "/services/data/v67.0/connect/setup/discovery/feature/sales-cloud-agent-studio/enable" \
87
+ --method POST \
88
+ --body '{}' \
89
+ --target-org <alias>
90
+ ```
91
+
92
+ Replace the path segment with the exact `featureApiName`. The endpoint itself
93
+ takes **no meaningful body** — but `sf api request rest --method POST` with no
94
+ `--body` flag at all fails with `Error (SfError): No 'mode' found in 'body'
95
+ entry` (verified on CLI 2.140.6 and 2.145.6 — this is a CLI-parser quirk, not
96
+ version-specific). Always pass `--body '{}'` explicitly. An optional
97
+ `solutionApiName` query param can bind the feature to a specific SKU/license
98
+ solution (omit for default — not used by this skill).
99
+
100
+ Response shape:
101
+
102
+ ```json
103
+ { "success": true }
104
+ ```
105
+
106
+ `{success:true}` = enabled (or was already enabled). If the response is an
107
+ error (e.g. `enableBlockedReasons` still present, missing license, unmet
108
+ dependency), record it verbatim and continue to the next toggle in the plan —
109
+ do not let one failure block the rest.
110
+
111
+ Capture every `/enable` response to a file and immediately hand it to
112
+ `scripts/record-enable-result.mjs` (below) — do not eyeball the JSON body in
113
+ prose to decide ENABLED vs FAILED.
114
+
115
+ ## Re-check before every toggle — never trust one snapshot for the whole loop
116
+
117
+ A toggle whose dependency is still off at read time carries a non-empty
118
+ `enableBlockedReasons` — but that dependency may be enabled by an *earlier*
119
+ step in this same loop. If the loop trusts the Phase-1/Phase-2 snapshot for
120
+ every toggle, a child that becomes enable-able mid-loop is permanently
121
+ written off as blocked. **Re-read `features/status` and re-run
122
+ `classify-enable-plan.mjs` immediately before each toggle attempt**, and
123
+ branch on that toggle's *current* signal:
124
+
125
+ - `PASS` → already enabled, skip.
126
+ - `FAIL` with empty `enableBlockedReasons` → POST `/enable`, then record the
127
+ result.
128
+ - `FAIL` with non-empty `enableBlockedReasons` → still genuinely blocked,
129
+ skip to the next toggle.
130
+ - `CANNOT-CONFIRM`/`ERROR` on that specific re-read → stop the loop and
131
+ surface the failure; do not guess.
132
+
133
+ One failed or blocked toggle does not stop the loop from attempting the
134
+ remaining toggles in `order`.
135
+
136
+ ## Enablement is confirm-gated — never a silent write
137
+
138
+ This skill mutates org state. Before any `/enable` POST, the exact `pending`
139
+ list (from the classifier) must be presented to the user via
140
+ `AskUserQuestion` and explicitly confirmed. Do not skip this checkpoint even
141
+ when the skill was reached via a hand-off from the validate skill's
142
+ NOT-READY report — that report names what is off, it does not constitute
143
+ confirmation to write.
144
+
145
+ ## The classifier — `scripts/classify-enable-plan.mjs`
146
+
147
+ The deterministic plan/verdict logic lives in the script, not in the workflow
148
+ prose (authoring standard A9). Invoke it with the captured file, the agent
149
+ type, and the captured exit status, using the skill's **absolute** directory:
150
+
151
+ ```bash
152
+ node "<skill_dir>/scripts/classify-enable-plan.mjs" /tmp/enable-status-before.json <fulfiller|employee> "$(cat /tmp/enable-status-before.exit)"
153
+ ```
154
+
155
+ Output (to stdout):
156
+
157
+ ```json
158
+ {
159
+ "agentType": "fulfiller | employee",
160
+ "readState": "ok | not-wired | error",
161
+ "features": { "<apiName>": { "status": "ENABLED|NOT_ENABLED|UNKNOWN", "signal": "PASS|FAIL|CANNOT-CONFIRM|ERROR", "enableBlockedReasons": [] } },
162
+ "order": ["<apiName>", "..."],
163
+ "alreadyEnabled": ["<apiName>", "..."],
164
+ "pending": ["<apiName>", "..."],
165
+ "blocked": ["<apiName>", "..."],
166
+ "unconfirmed": ["<apiName>", "..."],
167
+ "verdict": "ALL-ENABLED | NEEDS-ENABLE | CANNOT-CONFIRM | ERROR",
168
+ "reasons": ["..."],
169
+ "rawError": "null | <error snippet>"
170
+ }
171
+ ```
172
+
173
+ `unconfirmed` lists any required `apiName` that was missing from the response
174
+ or carried a status the classifier doesn't recognize — neither confirmed
175
+ `ENABLED` nor `NOT_ENABLED`. `verdict` is `NEEDS-ENABLE` whenever `pending` is
176
+ non-empty; otherwise `CANNOT-CONFIRM` whenever `unconfirmed` is non-empty; only
177
+ `ALL-ENABLED` when both are empty. An empty `pending` alone does **not** mean
178
+ every toggle is enabled — always check `unconfirmed` too.
179
+
180
+ Call the script at least **three times** across the flow: once on the Phase-2
181
+ read (before any write) to compute the confirm checkpoint and seed
182
+ `alreadyEnabled`; then **once per toggle, immediately before each attempt**,
183
+ inside the Phase-4 loop (see re-check rule above); and once more on the
184
+ Phase-5 re-read (after the enable loop) to feed `classify-final-report.mjs`.
185
+
186
+ ## Recording each enable attempt — `scripts/record-enable-result.mjs`
187
+
188
+ Deterministic per-toggle response classifier/accumulator (authoring standard
189
+ A9) — call it once per toggle, right after that toggle's `/enable` POST, from
190
+ inside the Bash loop:
191
+
192
+ ```bash
193
+ node "<skill_dir>/scripts/record-enable-result.mjs" /tmp/enable-<apiName>.json <apiName> /tmp/enable-results.json
194
+ ```
195
+
196
+ - `<response.json>` — the raw stdout captured from that toggle's `/enable`
197
+ POST.
198
+ - `<apiName>` — the featureApiName just attempted.
199
+ - `<results.json>` — an accumulator file read (if present) and rewritten each
200
+ call, keyed by `apiName`.
201
+
202
+ Emits/writes `{ "<apiName>": { "status": "ENABLED|FAILED", "reason": "null | <snippet>" } }` merged with any prior entries. `success:true` in the response body ⇒ `ENABLED`; any error body, empty body, or unexpected shape ⇒ `FAILED` with the reason captured verbatim. Never left unrecorded — even a malformed response becomes a `FAILED` entry, not a skipped one.
203
+
204
+ ## Final verdict — `scripts/classify-final-report.mjs`
205
+
206
+ Deterministic final-report aggregator (authoring standard A9) — run once,
207
+ after the Phase-5 re-read, combining the Phase-2 plan, the Phase-4 results
208
+ accumulator, and the Phase-5 re-classification:
209
+
210
+ ```bash
211
+ node "<skill_dir>/scripts/classify-final-report.mjs" /tmp/enable-plan-before.json /tmp/enable-results.json /tmp/enable-plan-after.json
212
+ ```
213
+
214
+ Pass `-` for the results arg if Phase 4 never ran (Phase-2 verdict was already
215
+ `ALL-ENABLED` or `CANNOT-CONFIRM`/`ERROR`). Output:
216
+
217
+ ```json
218
+ {
219
+ "features": { "<apiName>": { "finalStatus": "ALREADY-ENABLED|ENABLED|FAILED|CANNOT-CONFIRM|ERROR", "reason": "..." } },
220
+ "order": ["<apiName>", "..."],
221
+ "overall": "SUCCESS | PARTIAL | FAILED | CANNOT-CONFIRM | ERROR",
222
+ "reasons": ["..."]
223
+ }
224
+ ```
225
+
226
+ Render `features` and `overall` directly into the Output Format — do not
227
+ re-derive the per-feature or aggregate verdict in prose.
228
+
229
+ ## Error taxonomy
230
+
231
+ - **Auth error / `401 Unauthorized`** — session expired or wrong alias.
232
+ Re-run `sf org login web`; there is no token to refresh by hand.
233
+ - **`403 Forbidden`** — the user/org lacks the Agentforce license
234
+ (`accessCheck` gate). Surface verbatim.
235
+ - **`404 Not Found`** on `features/status` — the feature-discovery surface is
236
+ not wired on the org tier. The classifier maps a confirmed 404/NOT_FOUND
237
+ error body to CANNOT-CONFIRM.
238
+ - **Empty body / unexpected shape** — the classifier maps these to ERROR (not
239
+ CANNOT-CONFIRM): surface `rawError` and stop.
240
+ - **`enableBlockedReasons` non-empty** on a toggle — an unmet dependency or a
241
+ purchase/licensing gate; the classifier keeps the toggle out of the enable
242
+ loop (`blocked`) and reports the reason. Do not retry the POST — but do
243
+ re-check on the next loop iteration in case an earlier toggle's `/enable`
244
+ just unblocked it.
245
+ - **A required `apiName` missing from the response, or an unrecognized
246
+ `status` string** — neither confirmed `ENABLED` nor `NOT_ENABLED`; the
247
+ classifier lists it in `unconfirmed` and the verdict is `CANNOT-CONFIRM`
248
+ even if `pending` is empty. Never report `ALL-ENABLED`/`SUCCESS` while
249
+ `unconfirmed` is non-empty.
250
+
251
+ ## Gotchas
252
+
253
+ | Issue | Resolution |
254
+ |-------|------------|
255
+ | `sf api request rest --json` errors | Don't pass `--json` — the raw stdout body is already JSON |
256
+ | `sf api request rest --method POST` with no `--body` flag errors `No 'mode' found in 'body' entry` | The `/enable` endpoint takes no meaningful body, but the CLI still requires the flag — always pass `--body '{}'` explicitly (verified on CLI 2.140.6 and 2.145.6) |
257
+ | Bare `./scripts/classify-enable-plan.mjs` "not found" | Use the skill's absolute `<skill_dir>` in the `node` invocation |
258
+ | Enabling a child before its dependency | Always enable in the classifier's `order` — Einstein GenAI → Studio → parent → child template(s) |
259
+ | Attempting to enable a toggle whose *current* re-checked `enableBlockedReasons` is non-empty | Never — report the reasons instead of POSTing; the write would fail |
260
+ | Trusting the Phase-2 `blocked`/`pending` snapshot for the whole Phase-4 loop | Re-read + reclassify immediately before each toggle — a child blocked only because an earlier dependency was off becomes enable-able the instant that dependency's `/enable` succeeds |
261
+ | Reporting `ALL-ENABLED`/`SUCCESS` because `pending` is empty | Also check `unconfirmed` — a missing/unrecognized feature status is neither ENABLED nor NOT_ENABLED |
262
+ | Re-deriving the per-toggle or final aggregate verdict in prose | Use `record-enable-result.mjs` (per toggle) and `classify-final-report.mjs` (final) — deterministic scripts, not model judgment (authoring standard A9) |
263
+ | Skipping the confirm-to-write checkpoint on a hand-off from validate | Never — a NOT-READY report names what is off, it is not user confirmation to write |
264
+ | `updateOrgPref` → 500 `Invalid prefName` for an agent pref | Wrong write path — IPCManagement `updateOrgPref` is for IPC Management (Incident/Problem/Change) prefs only. Use `feature/{apiName}/enable` |
265
+ | Tempted to token+curl for "just a POST write" | Never — `sf api request rest` handles auth for `/services/data/...` paths |
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env node
2
+ // Deterministic enable-plan classifier for Agentforce for IT Service.
3
+ //
4
+ // Consumes the raw JSON body of a single `sf api request rest` read of the
5
+ // Salesforce Go feature-status endpoint and, for a chosen agent path
6
+ // (fulfiller | employee), computes a dependency-ordered enable plan and (on a
7
+ // second call, after the enable loop) the final per-feature verdict. This is
8
+ // the deterministic decision table that MUST NOT be interpreted by the model
9
+ // in prose (authoring standard A9) — same contract as the sibling
10
+ // service-itsm-agentic-setup-agentforce-studio-validate skill's
11
+ // classify-readiness.mjs, extended with enable ordering.
12
+ //
13
+ // Usage:
14
+ // node classify-enable-plan.mjs <features-status.json> <agentType> [exitStatus]
15
+ //
16
+ // <features-status.json> is a FILE PATH to the raw stdout captured from:
17
+ // sf api request rest "/services/data/vXX.0/connect/setup/discovery/features/status" \
18
+ // --method POST --body '{"featureApiNames":[...]}' --target-org <org> > file.json
19
+ // The body shape is { items: [ { apiName, status, enableBlockedReasons[],
20
+ // dependencyStatuses[] } ] } where status is "ENABLED" | "NOT_ENABLED". The
21
+ // file may instead hold the CLI error payload or be empty on a non-zero exit.
22
+ //
23
+ // <exitStatus> is the OPTIONAL numeric exit code of the `sf` command ($? in
24
+ // the shell) — distinguishes a confirmed 404 ("gate not wired" ->
25
+ // CANNOT-CONFIRM) from an auth/permission/transport failure (-> ERROR).
26
+ //
27
+ // <agentType> is one of: fulfiller | employee. Selects which path-specific
28
+ // template toggle is required in addition to the three shared toggles.
29
+ //
30
+ // Emits a single JSON object to stdout:
31
+ // { agentType, readState,
32
+ // features: { <apiName>: { status, signal, enableBlockedReasons } },
33
+ // order: [<apiName>...], // dependency-first order for the REQUIRED set
34
+ // alreadyEnabled: [<apiName>...],
35
+ // pending: [<apiName>...], // NOT_ENABLED, in dependency order — the enable plan
36
+ // blocked: [<apiName>...], // NOT_ENABLED with non-empty enableBlockedReasons
37
+ // unconfirmed: [<apiName>...], // missing from the response or an unexpected status
38
+ // verdict, reasons: [...], rawError }
39
+ // where verdict is ALL-ENABLED | NEEDS-ENABLE | CANNOT-CONFIRM | ERROR.
40
+ // Call this script BEFORE enabling (verdict=NEEDS-ENABLE -> drive the confirm
41
+ // list + enable loop from `pending`, in `order`) and AGAIN AFTER the enable
42
+ // loop on a fresh read (verdict=ALL-ENABLED -> success; anything left in
43
+ // `blocked` -> report enableBlockedReasons verbatim as FAILED for that toggle;
44
+ // anything in `unconfirmed` -> report CANNOT-CONFIRM for that toggle rather
45
+ // than assuming success — a required feature can be missing from the
46
+ // response or carry a status this classifier doesn't recognize, and an empty
47
+ // `pending` in that case does NOT mean every required toggle is ENABLED).
48
+ // Exit code is always 0 on a usable agentType; the verdict is in the payload.
49
+
50
+ import { readFileSync } from 'node:fs';
51
+
52
+ // Real Salesforce Go featureApiNames behind the "Agentforce for IT Service"
53
+ // setup page toggles — same apiNames the sibling validate skill classifies.
54
+ const EINSTEIN = 'sales-cloud-einstein-generative-ai'; // base layer (no deps)
55
+ const STUDIO = 'sales-cloud-agent-studio'; // "Turn on Agentforce Studio"
56
+ const PARENT = 'service-cloud-agentforce-for-itsm'; // "Agentforce for IT Service" umbrella
57
+ const FULFILLER = 'service-cloud-it-fulfiller-agent'; // "IT Service Fulfiller"
58
+ const REQUESTOR = 'service-cloud-requestor-agent'; // "IT Service Employee"
59
+ const SPECIALIZED = 'service-cloud-it-service-employee-agent'; // "Specialized Agent Templates for Employee"
60
+
61
+ // Dependency-first order per path: Einstein GenAI -> Studio -> parent umbrella -> child template(s).
62
+ const ORDER = {
63
+ fulfiller: [EINSTEIN, STUDIO, PARENT, FULFILLER],
64
+ employee: [EINSTEIN, STUDIO, PARENT, REQUESTOR, SPECIALIZED],
65
+ };
66
+
67
+ const LABEL = {
68
+ [EINSTEIN]: 'Einstein Generative AI',
69
+ [STUDIO]: 'Agentforce Studio',
70
+ [PARENT]: 'Agentforce for IT Service (parent)',
71
+ [FULFILLER]: 'IT Service Fulfiller template',
72
+ [REQUESTOR]: 'IT Service Employee template',
73
+ [SPECIALIZED]: 'Specialized Agent Templates for Employee',
74
+ };
75
+
76
+ function readJson(path) {
77
+ try {
78
+ const text = readFileSync(path, 'utf8').trim();
79
+ if (!text) return { ok: false, empty: true };
80
+ return { ok: true, data: JSON.parse(text) };
81
+ } catch {
82
+ return { ok: false, empty: false };
83
+ }
84
+ }
85
+
86
+ function asErrorArray(data) {
87
+ if (Array.isArray(data)) return data.filter((e) => e && (e.errorCode || e.message));
88
+ if (data && (data.errorCode || data.message)) return [data];
89
+ return [];
90
+ }
91
+
92
+ function isNotFoundError(errs) {
93
+ return errs.some((e) => {
94
+ const code = String(e?.errorCode ?? '').toUpperCase();
95
+ const msg = String(e?.message ?? '').toUpperCase();
96
+ return code.includes('NOT_FOUND') || code.includes('NOT_EXIST') ||
97
+ msg.includes('NOT FOUND') || msg.includes('DOES NOT EXIST') ||
98
+ msg.includes('COULD NOT BE FOUND');
99
+ });
100
+ }
101
+
102
+ function errorSnippet(errs) {
103
+ return errs.map((e) => `${e.errorCode ?? 'ERROR'}: ${e.message ?? ''}`.trim()).join(' | ').slice(0, 500);
104
+ }
105
+
106
+ function readFeatureStatus(res, exitStatus) {
107
+ if (!res.ok) {
108
+ const why = res.empty
109
+ ? (Number.isFinite(exitStatus) && exitStatus !== 0
110
+ ? `The read command exited ${exitStatus} with an empty body (auth / permission / transport failure).`
111
+ : 'The read produced an empty body — the response could not be read.')
112
+ : 'The read produced a non-JSON body that could not be parsed.';
113
+ return { readState: 'error', statusMap: null, rawError: why };
114
+ }
115
+ const d = res.data;
116
+ const errs = asErrorArray(d);
117
+ if (errs.length) {
118
+ if (isNotFoundError(errs)) return { readState: 'not-wired', statusMap: null, rawError: null };
119
+ return { readState: 'error', statusMap: null, rawError: errorSnippet(errs) };
120
+ }
121
+ const items = Array.isArray(d?.items) ? d.items : null;
122
+ if (!items) {
123
+ return { readState: 'error', statusMap: null, rawError: 'The response had no "items" array (unexpected shape).' };
124
+ }
125
+ const map = new Map();
126
+ for (const it of items) {
127
+ if (it?.apiName) {
128
+ map.set(it.apiName, {
129
+ status: String(it.status ?? '').toUpperCase(),
130
+ enableBlockedReasons: Array.isArray(it.enableBlockedReasons) ? it.enableBlockedReasons : [],
131
+ });
132
+ }
133
+ }
134
+ return { readState: 'ok', statusMap: map, rawError: null };
135
+ }
136
+
137
+ function classifyFeature(apiName, statusMap) {
138
+ const label = LABEL[apiName] ?? apiName;
139
+ if (!statusMap || !statusMap.has(apiName)) {
140
+ return { status: 'UNKNOWN', signal: 'CANNOT-CONFIRM', enableBlockedReasons: [], reason: `${label} (${apiName}) — no status returned.` };
141
+ }
142
+ const { status, enableBlockedReasons } = statusMap.get(apiName);
143
+ if (status === 'ENABLED') {
144
+ return { status, signal: 'PASS', enableBlockedReasons: [], reason: `${label} (${apiName}) → ENABLED.` };
145
+ }
146
+ if (status === 'NOT_ENABLED') {
147
+ const blocked = enableBlockedReasons.length > 0;
148
+ return {
149
+ status, signal: 'FAIL', enableBlockedReasons,
150
+ reason: blocked
151
+ ? `${label} (${apiName}) → NOT_ENABLED, blocked: ${enableBlockedReasons.map((r) => (typeof r === 'string' ? r : (r?.message ?? JSON.stringify(r)))).join('; ')}.`
152
+ : `${label} (${apiName}) → NOT_ENABLED (ready to enable).`,
153
+ };
154
+ }
155
+ return { status, signal: 'CANNOT-CONFIRM', enableBlockedReasons: [], reason: `${label} (${apiName}) → unexpected status "${status}".` };
156
+ }
157
+
158
+ const [statusPath, rawAgentType, rawExit] = process.argv.slice(2);
159
+ const agentType = String(rawAgentType ?? '').toLowerCase();
160
+ if (!statusPath || !ORDER[agentType]) {
161
+ process.stderr.write('usage: node classify-enable-plan.mjs <features-status.json> <fulfiller|employee> [exitStatus]\n');
162
+ process.exit(2);
163
+ }
164
+ const exitStatus = rawExit === undefined ? NaN : Number(rawExit);
165
+ const order = ORDER[agentType];
166
+
167
+ const { readState, statusMap, rawError } = readFeatureStatus(readJson(statusPath), exitStatus);
168
+
169
+ const features = {};
170
+ const reasons = [];
171
+ const alreadyEnabled = [];
172
+ const pending = [];
173
+ const blocked = [];
174
+
175
+ if (readState === 'error') {
176
+ for (const apiName of order) {
177
+ features[apiName] = { status: 'UNKNOWN', signal: 'ERROR', enableBlockedReasons: [] };
178
+ }
179
+ reasons.push(`Feature status could not be read: ${rawError} Surface the raw response and stop — do not treat as a wiring gap.`);
180
+ process.stdout.write(JSON.stringify({
181
+ agentType, readState, features, order, alreadyEnabled, pending, blocked, unconfirmed: order.slice(), verdict: 'ERROR', reasons, rawError,
182
+ }, null, 2) + '\n');
183
+ process.exit(0);
184
+ }
185
+
186
+ if (readState === 'not-wired') {
187
+ for (const apiName of order) {
188
+ features[apiName] = { status: 'UNKNOWN', signal: 'CANNOT-CONFIRM', enableBlockedReasons: [] };
189
+ }
190
+ reasons.push('The feature-discovery surface returned a confirmed 404 — not wired on this org tier.');
191
+ process.stdout.write(JSON.stringify({
192
+ agentType, readState, features, order, alreadyEnabled, pending, blocked, unconfirmed: order.slice(), verdict: 'CANNOT-CONFIRM', reasons, rawError,
193
+ }, null, 2) + '\n');
194
+ process.exit(0);
195
+ }
196
+
197
+ const unconfirmed = [];
198
+ for (const apiName of order) {
199
+ const c = classifyFeature(apiName, statusMap);
200
+ features[apiName] = { status: c.status, signal: c.signal, enableBlockedReasons: c.enableBlockedReasons };
201
+ reasons.push(c.reason);
202
+ if (c.signal === 'PASS') alreadyEnabled.push(apiName);
203
+ if (c.signal === 'FAIL') {
204
+ pending.push(apiName);
205
+ if (c.enableBlockedReasons.length) blocked.push(apiName);
206
+ }
207
+ if (c.signal === 'CANNOT-CONFIRM') unconfirmed.push(apiName);
208
+ }
209
+
210
+ // A required feature missing from the response, or carrying an unexpected
211
+ // status string, is neither confirmed ENABLED nor NOT_ENABLED — an empty
212
+ // `pending` list in that case must NOT read as ALL-ENABLED.
213
+ let verdict;
214
+ if (pending.length > 0) verdict = 'NEEDS-ENABLE';
215
+ else if (unconfirmed.length > 0) verdict = 'CANNOT-CONFIRM';
216
+ else verdict = 'ALL-ENABLED';
217
+
218
+ process.stdout.write(JSON.stringify({
219
+ agentType, readState, features, order, alreadyEnabled, pending, blocked, unconfirmed, verdict, reasons, rawError,
220
+ }, null, 2) + '\n');
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ // Deterministic final-report aggregator for the enable flow (authoring
3
+ // standard A9). Combines the Phase-2 (before-write) plan, the Phase-4
4
+ // per-toggle enable results (from record-enable-result.mjs), and the Phase-5
5
+ // (after-write) re-classification into one final per-feature verdict plus an
6
+ // overall SUCCESS/PARTIAL/FAILED/CANNOT-CONFIRM/ERROR summary — a fixed
7
+ // comparison/aggregation that must not be eyeballed by the model across a
8
+ // variable-length feature list.
9
+ //
10
+ // Usage:
11
+ // node classify-final-report.mjs <before.json> <results.json|-> <after.json>
12
+ //
13
+ // <before.json> — stdout of classify-enable-plan.mjs from Phase 2 (pre-write).
14
+ // <results.json> — stdout of record-enable-result.mjs accumulated across
15
+ // Phase 4 (one entry per toggle actually POSTed to /enable).
16
+ // Pass "-" if Phase 4 never ran (Phase-2 verdict was already
17
+ // ALL-ENABLED or CANNOT-CONFIRM/ERROR).
18
+ // <after.json> — stdout of classify-enable-plan.mjs from Phase 5 (post-write).
19
+ //
20
+ // Emits a single JSON object to stdout:
21
+ // { features: { <apiName>: { finalStatus, reason } }, order: [...], overall, reasons: [...] }
22
+ // where finalStatus is ALREADY-ENABLED | ENABLED | FAILED | CANNOT-CONFIRM | ERROR
23
+ // and overall is SUCCESS | PARTIAL | FAILED | CANNOT-CONFIRM | ERROR.
24
+ // Exit code is always 0 on usable args.
25
+
26
+ import { readFileSync } from 'node:fs';
27
+
28
+ const [beforePath, resultsPath, afterPath] = process.argv.slice(2);
29
+ if (!beforePath || !resultsPath || !afterPath) {
30
+ process.stderr.write('usage: node classify-final-report.mjs <before.json> <results.json|-> <after.json>\n');
31
+ process.exit(2);
32
+ }
33
+
34
+ function readJson(path) {
35
+ try {
36
+ return JSON.parse(readFileSync(path, 'utf8'));
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ const before = readJson(beforePath);
43
+ const after = readJson(afterPath);
44
+ const results = resultsPath === '-' ? {} : (readJson(resultsPath) ?? {});
45
+
46
+ if (!after || !Array.isArray(after.order)) {
47
+ process.stderr.write(`error: could not read/parse a usable "after" report from ${afterPath}\n`);
48
+ process.exit(3);
49
+ }
50
+
51
+ const alreadyEnabledBefore = new Set(Array.isArray(before?.alreadyEnabled) ? before.alreadyEnabled : []);
52
+ const order = after.order;
53
+ const features = {};
54
+ const reasons = [];
55
+
56
+ for (const apiName of order) {
57
+ const afterFeature = after.features?.[apiName];
58
+ const recorded = results?.[apiName];
59
+ const signal = afterFeature?.signal;
60
+
61
+ // The Phase-2 "already enabled" shortcut only applies if the Phase-5
62
+ // post-write read actually confirms it — a failed/unconfirmed re-read must
63
+ // never be silently overridden by the pre-write snapshot, or a broken
64
+ // verification read reports false SUCCESS for every already-enabled toggle.
65
+ if (alreadyEnabledBefore.has(apiName) && signal === 'PASS') {
66
+ features[apiName] = { finalStatus: 'ALREADY-ENABLED', reason: `${apiName} was already ENABLED before this run.` };
67
+ continue;
68
+ }
69
+
70
+ if (signal === 'PASS') {
71
+ features[apiName] = { finalStatus: 'ENABLED', reason: `${apiName} is ENABLED after this run.` };
72
+ } else if (signal === 'FAIL') {
73
+ const enableBlockedReasons = afterFeature?.enableBlockedReasons ?? [];
74
+ const recordedReason = recorded?.status === 'FAILED' ? recorded.reason : null;
75
+ features[apiName] = {
76
+ finalStatus: 'FAILED',
77
+ reason: recordedReason
78
+ ? `${apiName} — the /enable call failed: ${recordedReason}`
79
+ : enableBlockedReasons.length
80
+ ? `${apiName} — still blocked after the enable loop: ${JSON.stringify(enableBlockedReasons)}`
81
+ : `${apiName} — still NOT_ENABLED after the enable loop (not attempted or attempt did not persist).`,
82
+ };
83
+ } else if (signal === 'CANNOT-CONFIRM') {
84
+ features[apiName] = { finalStatus: 'CANNOT-CONFIRM', reason: `${apiName} — status could not be confirmed on the post-write read.` };
85
+ } else {
86
+ features[apiName] = { finalStatus: 'ERROR', reason: `${apiName} — the post-write read failed (${afterFeature?.reason ?? 'unknown error'}).` };
87
+ }
88
+ reasons.push(features[apiName].reason);
89
+ }
90
+
91
+ const statuses = Object.values(features).map((f) => f.finalStatus);
92
+ const hasError = statuses.includes('ERROR');
93
+ const hasFailed = statuses.includes('FAILED');
94
+ const hasCannotConfirm = statuses.includes('CANNOT-CONFIRM');
95
+ const hasEnabled = statuses.includes('ENABLED');
96
+
97
+ let overall;
98
+ if (hasError) overall = 'ERROR';
99
+ else if (hasFailed || hasCannotConfirm) overall = hasEnabled ? 'PARTIAL' : (hasFailed ? 'FAILED' : 'CANNOT-CONFIRM');
100
+ else overall = 'SUCCESS';
101
+
102
+ process.stdout.write(JSON.stringify({ features, order, overall, reasons }, null, 2) + '\n');
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ // Deterministic per-toggle enable-result recorder (authoring standard A9).
3
+ //
4
+ // Classifying a single `/enable` POST response as ENABLED vs FAILED, and
5
+ // accumulating that into a running results file across the enable loop, is a
6
+ // fixed decision — it must not be left to the model to eyeball each response
7
+ // in prose across a variable-length loop.
8
+ //
9
+ // Usage:
10
+ // node record-enable-result.mjs <response.json> <apiName> <results.json>
11
+ //
12
+ // <response.json> is a FILE PATH to the raw stdout captured from a single:
13
+ // sf api request rest ".../feature/<apiName>/enable" --method POST --body '{}' --target-org <alias>
14
+ // <results.json> is a FILE PATH this script reads (if present) and rewrites —
15
+ // an accumulator object keyed by apiName, so it can be called once per
16
+ // toggle, immediately after each individual (still-visible) Bash enable call.
17
+ //
18
+ // Prints the updated accumulator object to stdout on success. Exit code is
19
+ // always 0 on usable args — a failed enable is a recorded FAILED result, not
20
+ // a script failure.
21
+
22
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
23
+
24
+ const [responsePath, apiName, resultsPath] = process.argv.slice(2);
25
+ if (!responsePath || !apiName || !resultsPath) {
26
+ process.stderr.write('usage: node record-enable-result.mjs <response.json> <apiName> <results.json>\n');
27
+ process.exit(2);
28
+ }
29
+
30
+ function readJson(path) {
31
+ try {
32
+ const text = readFileSync(path, 'utf8').trim();
33
+ if (!text) return { ok: false, empty: true };
34
+ return { ok: true, data: JSON.parse(text) };
35
+ } catch {
36
+ return { ok: false, empty: false };
37
+ }
38
+ }
39
+
40
+ function asErrorArray(data) {
41
+ if (Array.isArray(data)) return data.filter((e) => e && (e.errorCode || e.message));
42
+ if (data && (data.errorCode || data.message)) return [data];
43
+ return [];
44
+ }
45
+
46
+ function errorSnippet(errs) {
47
+ return errs.map((e) => `${e.errorCode ?? 'ERROR'}: ${e.message ?? ''}`.trim()).join(' | ').slice(0, 500);
48
+ }
49
+
50
+ const res = readJson(responsePath);
51
+ let entry;
52
+ if (!res.ok) {
53
+ entry = { status: 'FAILED', reason: res.empty ? 'Enable call produced an empty body.' : 'Enable call produced a non-JSON body.' };
54
+ } else {
55
+ const errs = asErrorArray(res.data);
56
+ if (errs.length) {
57
+ entry = { status: 'FAILED', reason: errorSnippet(errs) };
58
+ } else if (res.data?.success === true) {
59
+ entry = { status: 'ENABLED', reason: null };
60
+ } else {
61
+ entry = { status: 'FAILED', reason: `Unexpected response shape (no success:true, no error body): ${JSON.stringify(res.data).slice(0, 500)}` };
62
+ }
63
+ }
64
+
65
+ let results = {};
66
+ if (existsSync(resultsPath)) {
67
+ const existing = readJson(resultsPath);
68
+ if (existing.ok && existing.data && typeof existing.data === 'object') results = existing.data;
69
+ }
70
+ results[apiName] = entry;
71
+
72
+ writeFileSync(resultsPath, JSON.stringify(results, null, 2), 'utf8');
73
+ process.stdout.write(JSON.stringify(results, null, 2) + '\n');