@salesforce/afv-skills 1.44.0 → 1.45.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 (97) hide show
  1. package/package.json +1 -1
  2. package/skills/consumer-goods-rtr-datacloud-export-configure/SKILL.md +72 -0
  3. package/skills/consumer-goods-rtr-datacloud-export-configure/references/inputs-and-namespace.md +38 -0
  4. package/skills/consumer-goods-rtr-datacloud-export-configure/references/procedure.md +158 -0
  5. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/detect-namespace.js +86 -0
  6. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/render-apex.js +64 -0
  7. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/resolve-id-by-name.js +47 -0
  8. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/sf-rest.js +171 -0
  9. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/soql-escape.js +26 -0
  10. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/upsert-report-config.apex +51 -0
  11. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/upsert-system-setting.apex +21 -0
  12. package/skills/consumer-goods-tpe-dashboard-configure/SKILL.md +74 -0
  13. package/skills/consumer-goods-tpe-dashboard-configure/references/phases-1-6.md +112 -0
  14. package/skills/consumer-goods-tpe-dashboard-configure/references/phases-7-12.md +157 -0
  15. package/skills/consumer-goods-tpe-dashboard-configure/scripts/find-failure-reason.js +132 -0
  16. package/skills/consumer-goods-tpe-dashboard-configure/scripts/poll-status.js +116 -0
  17. package/skills/consumer-goods-tpe-dashboard-configure/scripts/render-apex.js +64 -0
  18. package/skills/consumer-goods-tpe-dashboard-configure/scripts/run-data-transform.js +121 -0
  19. package/skills/consumer-goods-tpe-dashboard-configure/scripts/schedule-business-period-export.apex +27 -0
  20. package/skills/consumer-goods-tpe-dashboard-configure/scripts/sf-rest.js +171 -0
  21. package/skills/consumer-goods-tpe-dashboard-configure/scripts/soql-escape.js +25 -0
  22. package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/SKILL.md +141 -0
  23. package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/references/payload-shapes.md +447 -0
  24. package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/references/procedure.md +263 -0
  25. package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/scripts/clone-tpe-dashboards.js +537 -0
  26. package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/scripts/sf-rest.js +195 -0
  27. package/skills/consumer-goods-tpe-datakit-deploy/SKILL.md +157 -0
  28. package/skills/consumer-goods-tpe-datakit-deploy/scripts/detect-namespace.js +86 -0
  29. package/skills/consumer-goods-tpe-datakit-deploy/scripts/download-static-resource.js +151 -0
  30. package/skills/consumer-goods-tpe-datakit-deploy/scripts/extract-crm-field-permissions.js +115 -0
  31. package/skills/consumer-goods-tpe-datakit-deploy/scripts/sf-rest.js +109 -0
  32. package/skills/consumer-goods-tpe-datakit-deploy/scripts/update-field-permissions.js +433 -0
  33. package/skills/service-catalog-template-coordinate/SKILL.md +263 -0
  34. package/skills/service-catalog-template-coordinate/examples/output-templates.md +44 -0
  35. package/skills/service-catalog-template-coordinate/references/mcp-invocation.md +183 -0
  36. package/skills/service-catalog-template-coordinate/references/operations.md +230 -0
  37. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/SKILL.md +243 -0
  38. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/references/cli-invocation.md +205 -0
  39. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/references/helper-contracts.md +236 -0
  40. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/references/permset-topology.md +132 -0
  41. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-activated-agents.mjs +106 -0
  42. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-agent-access-state.mjs +113 -0
  43. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-assignment-state.mjs +99 -0
  44. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-platform-permset-availability.mjs +155 -0
  45. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/gate-unified-catalog-tiers.mjs +100 -0
  46. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/rank-candidate-users.mjs +95 -0
  47. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/resolve-target-user.mjs +86 -0
  48. package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +44 -23
  49. package/skills/service-itsm-agentic-setup-agentforce-coordinate/examples/output-templates.md +33 -9
  50. package/skills/service-itsm-agentic-setup-agentforce-studio-configure/SKILL.md +17 -18
  51. package/skills/service-itsm-agentic-setup-cmdb-coordinate/SKILL.md +3 -1
  52. package/skills/service-itsm-agentic-setup-configure/SKILL.md +20 -12
  53. package/skills/service-itsm-agentic-setup-configure/examples/output-templates.md +73 -5
  54. package/skills/service-itsm-agentic-setup-employee-agent-configure/SKILL.md +8 -7
  55. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/cli-invocation.md +45 -33
  56. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/reactivation.md +8 -6
  57. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/workflow-detail.md +10 -10
  58. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/classify-agent-existence.mjs +114 -56
  59. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/classify-preflight.mjs +33 -17
  60. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/render-report.mjs +9 -3
  61. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/SKILL.md +8 -7
  62. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/cli-invocation.md +43 -32
  63. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/reactivation.md +6 -4
  64. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/workflow-detail.md +10 -10
  65. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-agent-existence.mjs +106 -55
  66. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-preflight.mjs +27 -13
  67. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/render-report.mjs +9 -3
  68. package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +159 -161
  69. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone-action.json +51 -0
  70. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone.json +1 -1
  71. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/predefined-incident-policy.json +120 -0
  72. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/milestone-patterns.md +28 -5
  73. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/output-templates.md +19 -1
  74. package/skills/service-itsm-agentic-setup-incident-sla-configure/references/mcp-invocation.md +350 -30
  75. package/skills/service-itsm-channels-coordinate/SKILL.md +80 -213
  76. package/skills/service-itsm-slack-itservice-configure/SKILL.md +363 -0
  77. package/skills/service-itsm-slack-itservice-configure/references/connect-agentforce-to-slack.md +159 -0
  78. package/skills/service-itsm-slack-itservice-configure/references/manage-slack-connection.md +88 -0
  79. package/skills/service-itsm-slack-itservice-configure/references/manage-user-access.md +117 -0
  80. package/skills/service-itsm-slack-itservice-configure/references/record-visibility.md +78 -0
  81. package/skills/service-itsm-slack-itservice-configure/references/site-membership-verification.md +126 -0
  82. package/skills/service-itsm-slack-itservice-configure/scripts/classify-user-access.mjs +167 -0
  83. package/skills/service-itsm-teams-configure/SKILL.md +50 -47
  84. package/skills/service-itsm-teams-configure/references/azure-credential-population.md +42 -28
  85. package/skills/service-itsm-teams-configure/references/gotchas.md +1 -2
  86. package/skills/service-itsm-teams-coordinate/SKILL.md +22 -18
  87. package/skills/service-itsm-teams-coordinate/examples/output-templates.md +12 -9
  88. package/skills/service-itsm-teams-itdesk-configure/SKILL.md +60 -44
  89. package/skills/service-itsm-teams-itservice-configure/SKILL.md +56 -70
  90. package/skills/service-catalog-template-deploy/SKILL.md +0 -310
  91. package/skills/service-catalog-template-deploy/references/cli-invocation.md +0 -258
  92. package/skills/service-catalog-template-deploy/scripts/activate-verify.mjs +0 -164
  93. package/skills/service-catalog-template-deploy/scripts/build-deploy-payload.mjs +0 -94
  94. package/skills/service-catalog-template-deploy/scripts/resolve-template.mjs +0 -331
  95. package/skills/service-catalog-template-search/SKILL.md +0 -212
  96. package/skills/service-catalog-template-search/references/cli-invocation.md +0 -128
  97. package/skills/service-catalog-template-search/scripts/classify-catalog.mjs +0 -205
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/afv-skills",
3
- "version": "1.44.0",
3
+ "version": "1.45.0",
4
4
  "description": "Salesforce skills for Agentforce Vibes",
5
5
  "license": "CC-BY-NC-4.0",
6
6
  "files": [
@@ -0,0 +1,72 @@
1
+ ---
2
+ name: consumer-goods-rtr-datacloud-export-configure
3
+ description: "Configure a Trade Promotion Management (TPM) org's RTR (Real Time Reporting) export pipeline to push promotion measures into Data Cloud (Data 360) for Trade Promotion Effectiveness (TPE) dashboards. Use when a customer or admin wants to set up the RTR export for Data Cloud measures, configure the RTR report for TPE, or as a delegated step from consumer-goods-tpe-dashboard-configure."
4
+ metadata:
5
+ version: "1.0"
6
+ domains: ["Consumer Goods"]
7
+ relatedSkills:
8
+ - "consumer-goods-tpe-dashboard-configure"
9
+ cliTools:
10
+ - tool: ["node"]
11
+ semver: ">=20.0.0"
12
+ - tool: ["sf"]
13
+ semver: ">=2.0.0"
14
+ ---
15
+
16
+ # Setup RTR Data Cloud Export
17
+
18
+ Configures the Data-Cloud-targeted RTR exports: the `Data_Cloud_Enabled` system setting, a scoped SF Data Sync, an `RTR_Report_Configuration__c` record (with its `KPI_Set__c`) for Data Cloud export, and the export trigger/status/abort REST calls. Standalone and self-contained — runs on its own, or as a delegated step invoked by `consumer-goods-tpe-dashboard-configure`, which passes through its own already-collected username, dry-run flag, and GenieAdmin confirmation result.
19
+
20
+ **Distinct from a sibling CSV-export RTR skill** (if present in this org's checkout): that skill targets `Usage__c IN ('integrationmeta','reportmeta')` for CSV export to the CGCloud Processing Services host. This skill targets Data Cloud export. Same underlying `RTR_Report_Configuration__c` object and `KPI_Set__c` Apex patterns, different `Usage__c` values and `JSON__c` shape — this skill does not invoke or depend on that sibling.
21
+
22
+ **For now, this skill only supports `Usage__c = 'datacloudpromotionmeasuresmeta'`** (`JSON__c.object = "promotionmeasures"`) — it is hardcoded, never asked as an input. `datacloudaccountplanmeasuresmeta` (account-plan measures) is not yet wired up.
23
+
24
+ ## Inputs and namespace detection
25
+
26
+ Read `references/inputs-and-namespace.md` before starting. It covers:
27
+ - The username/dry-run/GenieAdmin/TPM-Admin/namespace inputs that must be collected (or passed through by a calling skill) before Phase 1.
28
+ - The Sales Org / KPI Set / measure-codes / date-from inputs that Phase 6 collects lazily, per Sales Org, only once its existing-config check comes back empty.
29
+ - The namespace-detection procedure (`scripts/detect-namespace.js`) and the four `NS`/`NS_SEGMENT`/`NS_APEX`/`NS_FIELD` derivations used throughout every phase below.
30
+ - `scripts/render-apex.js` (Apex template rendering) and `scripts/resolve-id-by-name.js` (name-to-id lookups) both load `scripts/soql-escape.js` to escape substituted/queried literals for safe SOQL/Apex interpolation.
31
+
32
+ ## Phases
33
+
34
+ Full step-by-step commands, queries, and Apex templates for every phase are in `references/procedure.md`. Run phases in order.
35
+
36
+ | Phase | Summary |
37
+ |-------|---------|
38
+ | 1 | Verify/assign `GenieAdmin` permission set; confirm `TPM Admin` persona assignment. |
39
+ | 2 | Upsert the `Data_Cloud_Enabled` system setting via anonymous Apex; confirm the write. |
40
+ | 3 | Trigger a scoped SF Data Sync against `System_Setting__c`, after capturing a baseline `Batch_Run_Status__c` Id. |
41
+ | 4 | Poll/verify that sync completed (`Done`/`Error`/`Fatal`/`Running`). |
42
+ | 5 | Verify the `datacloudpromotionmeasuresmeta` picklist value exists on `Usage__c` — if missing, stop and send the user to Setup (no programmatic fix exists). |
43
+ | 6 | Loop, one Sales Org at a time: resolve Sales Org Name → Id, reuse an existing `RTR_Report_Configuration__c` or collect KPI Set/measure-codes/date-from and create one, sync if changed, then ask whether to add another Sales Org. |
44
+ | 7 | Once Phase 6's loop is fully done, trigger the export once per configured Sales Org via `scripts/sf-rest.js` (never `sf api request rest`); capture `requestId` as the export guid. |
45
+ | 8 | Poll each Sales Org's export status (`Ready`/`Error`/`Aborted`/`Queued`/`InProgress`) via `scripts/sf-rest.js`. |
46
+ | 9 | Once every Sales Org has finished polling, ask the user to sync the `promotionmeasures` Data Cloud Data Stream themselves and get explicit confirmation it finished — this skill never triggers that sync. |
47
+ | 10 | Abort an in-flight export via `scripts/sf-rest.js` — only if the user explicitly asks. |
48
+ | 11 | Report pass/blocked/pending per phase. |
49
+
50
+ ## Rules
51
+
52
+ - Never assign a permission set, write a record, or trigger a sync/export without an explicit user go-ahead first.
53
+ - Never guess or hardcode `Batch_Name__c` — identify the sync's `Batch_Run_Status__c` row by recency against the captured baseline Id, and log the full row set.
54
+ - Never attempt to programmatically add a missing `Usage__c` picklist value — the user must add it manually via Setup (link in Phase 5, `references/procedure.md`).
55
+ - Never ask for a Usage type — this skill is hardcoded to `Usage__c = 'datacloudpromotionmeasuresmeta'` / `JSON__c.object = "promotionmeasures"` for now.
56
+ - When the requested export type is anything other than promotion measures (e.g. `datacloudaccountplanmeasuresmeta`/account-plan measures): state the limitation, then stop or ask if the user wants the promotion-measures export instead for that Sales Org. Do not provide field-by-field manual configuration instructions for the unsupported path — no suggested `Usage__c`/`JSON__c` values, no picklist-add walkthrough. Offering a workaround recipe for the exact path this skill refuses is scope creep, not help. Report the run as blocked, not partially-passed.
57
+ - Never skip Phase 6's existence check (keyed on `Usage__c` + `Sales_Organization__c`) — and never overwrite an existing `RTR_Report_Configuration__c` found by that check without a separate, explicit user go-ahead.
58
+ - Never trigger Phase 7's export before Phase 6's per-Sales-Org loop is fully done — always ask whether to add another Sales Org before moving from Phase 6 to Phase 7.
59
+ - Always give the user a chance to supply a custom `Internal_Name__c` (Phase 6 step 4) — default to `"Data 360 Promotion Measures Export <Sales Org Name>"` (no hyphen) only if they decline. This value is also `metaname` in Phase 7's export — never invent a different one for that call.
60
+ - Phase 7 exports **once per Sales Org**, never once for the whole run.
61
+ - Both the Sales Org and the KPI Set are given as **Names** by the user, resolved to Ids by query — this skill never creates a `Sales_Organization__c`, a `KPI_Set__c`, or KPI Set junction rows.
62
+ - Never sync a Sales Org's config in Phase 6 unless this run actually created it — reusing an existing config as-is is a no-op, no sync call.
63
+ - Never call the abort endpoint unless the user explicitly asks to abort.
64
+ - Never claim Phase 9 is done on Phase 8's `Ready` status alone — always get the user's explicit confirmation that the `promotionmeasures` Data Stream was synced, once for the whole batch, before moving to Phase 10.
65
+ - Never hardcode the namespace in a REST path, Apex snippet, or SOQL query — always use the caller-passed or self-detected `NS`/`NS_SEGMENT`/`NS_APEX`/`NS_FIELD`.
66
+ - This skill only calls `global` Apex classes, REST endpoints, or the anonymous-Apex templates in its own `scripts/` directory — never a `public`/`private` method directly, and never a script or template from outside this skill's own directory.
67
+
68
+ ## Report
69
+
70
+ At the end of a run, give the user a structured status per phase (1-10): pass / blocked / pending, with the specific blocking reason and remediation click-path where applicable (e.g. the Setup path for a missing picklist value). Call out anything skipped due to dry-run or a declined confirmation.
71
+
72
+ **When invoked via delegation** (the calling skill used the `Skill` tool to reach this file, rather than the user directly): this report is an intermediate result, not the end of the task. Return it to the calling skill and let it continue with its own next phase — do not present this report to the user as the final answer and stop. Only surface this report directly to the user when this skill was invoked standalone.
@@ -0,0 +1,38 @@
1
+ # Inputs to collect first
2
+
3
+ Ask before starting, unless already supplied by a calling skill. Do not guess any of these.
4
+
5
+ 1. **TPM System Admin username** — already authenticated via `sf` CLI. Verify via the sibling script, never `sf org display --json` directly — that command's raw output includes `accessToken` (a live credential), which this script strips before printing:
6
+ ```bash
7
+ node ./scripts/sf-rest.js org-status --target-org <username>
8
+ ```
9
+ Prints `{"username","alias","connectedStatus","orgId"}`. Non-zero exit or `connectedStatus != "Connected"` ⇒ stop, ask the user to log in themselves.
10
+ 2. **Dry-run?** — offer by default. In dry-run, every read/verify step still runs; every write/trigger step is skipped and recorded as `pending — not run (dry-run)`.
11
+ 3. **GenieAdmin confirmation** — if invoked standalone, query it directly (see Phase 1 in `references/procedure.md`); if invoked by `consumer-goods-tpe-dashboard-configure`, use its passed-through result instead of re-querying.
12
+ 4. **"TPM Admin" permission-set confirmation** — org-specific, cannot be verified programmatically. Ask directly if run standalone; if invoked by `consumer-goods-tpe-dashboard-configure`, use its passed-through confirmation.
13
+ 5. **Namespace (`NS`)** — if invoked by `consumer-goods-tpe-dashboard-configure`, use its passed-through `NS` (raw namespace string, already detected once for the whole run — never re-detect). If run standalone, detect it once yourself, before Phase 1 (see "Namespace detection" below).
14
+
15
+ Do **not** ask for anything below yet. None of these are collected up front — Phase 6 asks for the Sales Org Name itself as its first step of each loop iteration, then checks for an existing `RTR_Report_Configuration__c` before asking for the rest:
16
+
17
+ 6. **Sales Org** — a `Sales_Organization__c` **Name**, provided directly by the user; the skill resolves it to an Id by query (Phase 6 step 1). Phase 6 processes Sales Orgs one at a time, asking for this Name at the start of each loop iteration and asking whether to add another after each one finishes.
18
+ 7. **KPI Set** — the **Name** of an existing `KPI_Set__c`, provided by the user; the skill resolves it to an Id by query (Phase 6 step 3). This skill never creates a KPI Set — customers set up their `KPI_Set__c` and its `KPI_Set_KPI_Definition__c` junction rows independently, outside this skill.
19
+ 8. **Measure codes** — the KPI Definition measure codes to export, not Names. These are pure user input, exactly like Sales Org and KPI Set above — never propose, default, or pre-select any codes yourself, even as an "example" answer option. Once the KPI Set is resolved in Phase 6 step 3, query that KPI Set's junction rows, filtered to writeback-eligible KPI Definitions only (`NS_FIELD` throughout):
20
+ ```bash
21
+ sf data query --target-org <username> \
22
+ --query "SELECT <NS_FIELD>KPI_Definition__r.Name, <NS_FIELD>KPI_Definition__r.<NS_FIELD>Storage_Options_Measure_Code__c FROM <NS_FIELD>KPI_Set_KPI_Definition__c WHERE <NS_FIELD>KPI_Set__c = '<kpiSetId>' AND <NS_FIELD>KPI_Definition__r.<NS_FIELD>Storage_Options_Writeback__c = true"
23
+ ```
24
+ Zero rows ⇒ stop and tell the user this KPI Set has no writeback-eligible KPI Definitions (none with `Storage_Options_Writeback__c = true`); they need to enable writeback on at least one KPI Definition in the set before this export can be configured. Otherwise, show the user the resulting list of `{Name, Storage_Options_Measure_Code__c}` pairs and ask: *"Export all N writeback-eligible KPIs listed above, or a specific subset? Note: the amount of data exported is proportional to the number of KPIs selected — fewer codes means a smaller, faster export."* "All" ⇒ use every `Storage_Options_Measure_Code__c` from the list. Subset ⇒ let the user pick specific Names/codes from that same list — never accept a code that didn't appear in the query result. Used only to populate `JSON__c`'s `measurecodes` list; not used to build junction rows.
25
+ 9. **Date-from** — export start date, `YYYY-MM-DD`.
26
+
27
+ ## Namespace detection (standalone runs only — skip if `NS` was passed in)
28
+
29
+ Same canonical procedure as `consumer-goods-tpe-dashboard-configure`'s "Namespace detection" step (package-installed lookup, falling back to `sf org display`'s `result.namespace`, falling back to `""`) — never re-derive a different way. Run the sibling script, which does all three steps and the `NS_*` derivations in one call:
30
+ ```bash
31
+ node ./scripts/detect-namespace.js --target-org <username>
32
+ ```
33
+ Prints `{"NS","NS_SEGMENT","NS_APEX","NS_FIELD"}` as one line of JSON. Store all four and reuse everywhere below:
34
+
35
+ - `NS` — raw namespace string (may be empty).
36
+ - `NS_SEGMENT` — REST URL path segment: `"<NS>/"` if `NS` is non-empty, else `""`. Insert immediately after `/services/apexrest/`.
37
+ - `NS_APEX` — Apex class-reference prefix: `"<NS>."` if `NS` is non-empty, else `""`. Prefix every reference to a class from this package (`OffPlatformCallout`, `OffPlatformCalloutResponse`, `TransactionHandler`, `RTRSyncReportConfigInvocable`) with this.
38
+ - `NS_FIELD` — custom object/field API-name prefix: `"<NS>__"` if `NS` is non-empty, else `""`. Prefix every custom object and custom field API name (standard fields like `Id`/`Name` never take this prefix) with this, in SOQL, `sf sobject describe`/`sf data upsert` commands, and Apex sObject type declarations alike.
@@ -0,0 +1,158 @@
1
+ # Procedure
2
+
3
+ Before starting, read `references/inputs-and-namespace.md` for the required inputs and the namespace-detection procedure.
4
+
5
+ ## Phase 1 — Permission sets
6
+
7
+ 1. **GenieAdmin** — if a value was passed in from the caller, use it. If standalone, resolve its Label first (never show the API `Name` to the user), then check the assignment:
8
+ ```bash
9
+ sf data query --target-org <username> --query "SELECT Label FROM PermissionSet WHERE Name = 'GenieAdmin'"
10
+ sf data query --target-org <username> \
11
+ --query "SELECT PermissionSet.Name FROM PermissionSetAssignment WHERE Assignee.Username = '<username>' AND PermissionSet.Name = 'GenieAdmin'"
12
+ ```
13
+ Missing ⇒ tell the user the Label from the first query (e.g. *"User <username> is missing the &lt;Label&gt; permission set. Assign it now?"*) and ask permission to assign before continuing (same pattern as `consumer-goods-tpe-dashboard-configure` Phase 3).
14
+ 2. **TPM Admin** — never re-derive or guess. Use the passed-in confirmation, or ask directly if standalone: *"Confirm the permission set(s) required for the org's TPM Admin persona are assigned to the username."* Wait for an explicit yes.
15
+
16
+ ## Phase 2 — `Data_Cloud_Enabled` system setting
17
+
18
+ **Inform the user first — this is a global setting other parts of the org may read.** Tell them plainly: *"This will create/update the System_Setting__c record named 'Data_Cloud_Enabled' with Value__c = 'true'."* Wait for a go-ahead. `Value__c` is a text field (not Boolean) — the literal string `'true'` is written, not the Boolean `true`. There is no single-record `sf data upsert record` command in the current `sf` CLI (`sf data upsert` only has `bulk`/`resume` CSV subcommands), so this uses an idempotent anonymous-Apex upsert instead — query-then-insert-or-update inside one transaction, avoiding the query/create/update round-trip race the CLI approach would have. `%NS%` takes the `NS_FIELD` form:
19
+ ```bash
20
+ node ./scripts/render-apex.js scripts/upsert-system-setting.apex --var NS=<NS_FIELD>
21
+ sf apex run --target-org <username> -f <rendered-path> --json
22
+ ```
23
+ Read the record Id back from the debug log line `RTR_DATACLOUD_SETTING_ID=<id>`. Then query to confirm the write landed, `NS_FIELD` on the custom object/field (`Name` is standard, no prefix):
24
+ ```bash
25
+ sf data query --target-org <username> \
26
+ --query "SELECT Id, Name, <NS_FIELD>Value__c FROM <NS_FIELD>System_Setting__c WHERE Name = 'Data_Cloud_Enabled'"
27
+ ```
28
+ Confirm exactly one row with `Value__c = 'true'`. Dry-run ⇒ skip both the execute and the confirmation query, record `pending — not run (dry-run)`.
29
+
30
+ ## Phase 3 — Trigger SF Data Sync (scoped)
31
+
32
+ Capture a baseline `Batch_Run_Status__c` Id first, so Phase 4's poll can't match a stale prior sync run (`NS_FIELD` on the custom object; `Id`/standard fields unprefixed):
33
+ ```bash
34
+ sf data query --target-org <username> \
35
+ --query "SELECT Id FROM <NS_FIELD>Batch_Run_Status__c ORDER BY <NS_FIELD>Start_Date__c DESC LIMIT 1" --json
36
+ ```
37
+ Store as `LAST_BRS_ID` (empty if zero rows). Then run, scoped to `System_Setting__c` (the object Phase 2 wrote) rather than a full-org sync. `OffPlatformCallout` takes `NS_APEX`; the tracked object name inside the JSON string is a field API name and takes `NS_FIELD`; the transaction id comes from the confirmed-`global` `TransactionHandler.getTransactionIdentifier()` (also `NS_APEX`-prefixed), not `TPM_App.getTxId()` — `TPM_App` is only `public`, unreachable from anonymous Apex against a namespaced managed-package install:
38
+ ```apex
39
+ <NS_APEX>OffPlatformCallout callout = new <NS_APEX>OffPlatformCallout('SCHEDULE_SFDATA_SYNC', null);
40
+ <NS_APEX>OffPlatformCalloutResponse resp = callout.execute(
41
+ <NS_APEX>TransactionHandler.getTransactionIdentifier(), null,
42
+ '{"command":"SYNC","trackedObjectApiNames":["<NS_FIELD>System_Setting__c"]}'
43
+ );
44
+ System.debug(resp);
45
+ ```
46
+ via `sf apex run`. Use `'RE_SYNC'` instead of `'SYNC'` only if resyncing the same scoped object list. Only omit `trackedObjectApiNames` entirely (plain `{"command":"SYNC"}`) if the user explicitly asks for a full-org sync instead. Dry-run ⇒ skip, record `pending — not run (dry-run)`.
47
+
48
+ ## Phase 4 — Verify the sync completed
49
+
50
+ ```bash
51
+ sf data query --target-org <username> \
52
+ --query "SELECT Id, <NS_FIELD>Batch_Name__c, <NS_FIELD>Batch_State__c, <NS_FIELD>Start_Date__c, <NS_FIELD>End_Date__c FROM <NS_FIELD>Batch_Run_Status__c WHERE Id > '<LAST_BRS_ID>' ORDER BY <NS_FIELD>Start_Date__c DESC"
53
+ ```
54
+ Log every returned row — the exact `Batch_Name__c` is `SFDataSyncWorker`. Terminal: `Done` = success, `Error`/`Fatal` = failure, `Running` = poll again after a short wait.
55
+
56
+ ## Phase 5 — Verify the `Usage__c` picklist value exists
57
+
58
+ ```bash
59
+ sf sobject describe --sobject <NS_FIELD>RTR_Report_Configuration__c --target-org <username> --json
60
+ ```
61
+ Confirm `datacloudpromotionmeasuresmeta` appears in the `Usage__c` field's `picklistValues[]`.
62
+
63
+ **If missing, do not try to patch it programmatically.** Picklist values are not carried by a metadata deploy or package upgrade into a subscriber org (confirmed Salesforce platform behavior) — this skill has no way to append a picklist value without a full read-modify-write of the field, which is more fragile than the manual step. Tell the user to add it manually: Setup → Object Manager → RTR Report Configuration → Fields & Relationships → Usage → New → add `datacloudpromotionmeasuresmeta` ("Data 360 Promotion Measures Export"). Reference: https://help.salesforce.com/s/articleView?id=ind.tpm_add_report_metadata.htm&type=5. Then re-run this check before continuing to Phase 6.
64
+
65
+ ## Phase 6 — Reuse or create the RTR Report Configuration (one Sales Org at a time)
66
+
67
+ `RTR_Report_Configuration__c` is unique per (`Usage__c`, `Sales_Organization__c`) pair. This skill always uses `Usage__c = 'datacloudpromotionmeasuresmeta'` — hardcoded, never asked. Customers can configure more than one Sales Org under it — process them **one at a time**: run steps 1-6 below for the current Sales Org, then ask *"Add another Sales Org to configure?"* before moving on. The Sales Org Name itself is **not** collected up front — step 1 below asks for it fresh on each loop iteration. Check for an existing config **before** asking for anything else — do not ask for KPI Set/measure codes/date-from until the check comes back empty for this Sales Org. Keep a running `CONFIGURED_SALES_ORGS` list of `{salesOrgId, salesOrgName, internalName}` for every Sales Org this loop finishes (both the reused-as-is and the newly-created case) — Phase 7 exports **once per Sales Org in that list**, using each one's `internalName` as `metaname`, only after this loop is fully done.
68
+
69
+ The remaining Apex template's `%NS%` placeholder expects the `NS_FIELD` form (double-underscore, e.g. `cgcloud__`, or empty) — pass the already-detected `NS_FIELD` value as `--var NS=<NS_FIELD>` in every `render-apex.js` call below (never re-detect; use the value from "Namespace detection" / the caller's passed-through `NS`).
70
+
71
+ This skill never creates a `KPI_Set__c` or its `KPI_Set_KPI_Definition__c` junction rows — customers set those up independently. Both the Sales Org and the KPI Set are given as **Names** by the user; the skill resolves each to an Id by query (steps 1 and 3) before step 4 runs.
72
+
73
+ 1. **Ask for this iteration's Sales Org Name**, then resolve it to an Id, `NS_FIELD` on the custom object (`Id`/`Name` are standard, unprefixed):
74
+ ```bash
75
+ node ./scripts/resolve-id-by-name.js --target-org <username> --object <NS_FIELD>Sales_Organization__c --name "<salesOrgName>"
76
+ ```
77
+ Zero rows ⇒ stop and tell the user no `Sales_Organization__c` with that Name exists; ask for a different Name. More than one row ⇒ show the matches and ask the user to disambiguate. Exactly one ⇒ use its Id as `<salesOrgId>`.
78
+ 2. **Check for an existing config, per Sales Org Id**, `NS_FIELD` on the custom object/fields:
79
+ ```bash
80
+ sf data query --target-org <username> \
81
+ --query "SELECT Id, Name, <NS_FIELD>Internal_Name__c, <NS_FIELD>Reporting_KPI_Set__c, <NS_FIELD>JSON__c FROM <NS_FIELD>RTR_Report_Configuration__c WHERE <NS_FIELD>Usage__c = 'datacloudpromotionmeasuresmeta' AND <NS_FIELD>Sales_Organization__c = '<salesOrgId>'"
82
+ ```
83
+ - **Found** ⇒ show the user the existing config's Name/Id and current `Internal_Name__c`/`Reporting_KPI_Set__c`/`JSON__c`, and ask them to confirm proceeding with it as-is. Confirmed ⇒ record its Id as `<configId>` and its `Internal_Name__c` as `<internalName>` for this Sales Org, mark it **unchanged**, and skip straight to step 6 (loop control) — no sync call for this Sales Org, since nothing was modified. Never ask for KPI Set/measure codes/date-from for this Sales Org, and never overwrite it here. Declined ⇒ skip this Sales Org for this run and note it in the report; don't fall through to rebuilding it without a separate, explicit ask.
84
+ - **Not found** ⇒ now ask for this Sales Org's remaining build inputs (KPI Set Name, measure codes, date-from — see `references/inputs-and-namespace.md`) and continue with steps 3-6 below for this Sales Org.
85
+ 3. **Resolve the KPI Set Name to an Id**, `NS_FIELD` on the custom object (`Id`/`Name` are standard, unprefixed):
86
+ ```bash
87
+ node ./scripts/resolve-id-by-name.js --target-org <username> --object <NS_FIELD>KPI_Set__c --name "<kpiSetName>"
88
+ ```
89
+ Zero rows ⇒ stop and tell the user no `KPI_Set__c` with that Name exists; ask them to create it (with its measure-code junction rows) first, or give a different Name. More than one row ⇒ show the matches and ask the user to disambiguate. Exactly one ⇒ use its Id as `<kpiSetId>`.
90
+ 4. **Determine the Internal Name, then create the RTR Report Configuration** — this Sales Org has no existing row (step 2 confirmed it), so this always inserts; the template's own existence check is just an idempotency safety net if this phase is re-run. **Ask the user** whether they want to supply a custom `Internal_Name__c` for this Sales Org's config, or use the default: `"Data 360 Promotion Measures Export <Sales Org Name>"` (no hyphen — whether one would trip a validation rule on this field is unconfirmed, so it's avoided), where `<Sales Org Name>` is the `Name` resolved in step 1 — e.g. `"Data 360 Promotion Measures Export US01"`. (A bare label with no Sales Org suffix is only correct when there's exactly one Sales Org configured; once a second one is added, its row needs its own distinguishable label — flag this to the user if they supply a custom name that doesn't vary per Sales Org.) Record whichever value is used as `<internalName>` for this Sales Org — Phase 7 passes it as `metaname` when triggering that Sales Org's export. Assemble `JSON__c` next:
91
+ ```json
92
+ {"measurecodes":["<code1>","<code2>"],"datefrom":"<YYYY-MM-DD>","object":"promotionmeasures"}
93
+ ```
94
+ Then render and run (pass the JSON raw — `render-apex.js` escapes every `--var` value for you; pre-escaping it here would double-escape):
95
+ ```bash
96
+ node ./scripts/render-apex.js scripts/upsert-report-config.apex \
97
+ --var NS=<NS_FIELD> --var USAGE=datacloudpromotionmeasuresmeta --var INTERNAL_NAME="<internalName>" \
98
+ --var SALES_ORG_ID=<salesOrgId> --var KPI_SET_ID=<kpiSetId> --var JSON_LITERAL="<json>"
99
+ sf apex run --target-org <username> -f <rendered-path> --json
100
+ ```
101
+ Read the record Id back from `RTR_DATACLOUD_CONFIG_ID=<id>`. Mark this Sales Org **changed**.
102
+ 5. **Sync it — only if changed.** If step 2 found an existing config and the user confirmed reuse as-is, there is nothing to sync — skip this step entirely for that Sales Org. Only run this after step 4 actually created a new config:
103
+ ```apex
104
+ <NS_APEX>RTRSyncReportConfigInvocable.syncReportConfig(
105
+ new List<<NS_FIELD>RTR_Report_Configuration__c>{ [SELECT Id FROM <NS_FIELD>RTR_Report_Configuration__c WHERE Id = '<configId>'] }
106
+ );
107
+ ```
108
+ via `sf apex run`. `RTRSyncReportConfigInvocable` (an Apex class) takes `NS_APEX`; `RTR_Report_Configuration__c` (the sObject) takes `NS_FIELD`. Sync happens here, immediately after this Sales Org's config is created — never deferred to a later batch step.
109
+ 6. Add `{salesOrgId, salesOrgName, internalName}` for this Sales Org to `CONFIGURED_SALES_ORGS`, then ask: *"Add another Sales Org to configure?"* Yes ⇒ go back to step 1 with the next Sales Org Name. No ⇒ move to Phase 7. Dry-run ⇒ skip Phase 6 entirely (for every Sales Org), record `pending — not run (dry-run)`.
110
+
111
+ ## Phase 7 — Trigger the export (once per Sales Org, only after Phase 6's loop is done)
112
+
113
+ **Never trigger this until Phase 6's loop has finished for every Sales Org the user wants configured this run** — exporting mid-loop would run against an incomplete `CONFIGURED_SALES_ORGS` list. Once done, run once per `{salesOrgName, internalName}` entry in that list — every Sales Org gets its own export call, never a single shared one. `metaname` is that entry's `internalName` (the same value written to `Internal_Name__c` for that Sales Org's config in Phase 6) — this ties the export run back to the specific `RTR_Report_Configuration__c` it was scheduled for. Never use `sf api request rest` — it's a beta command with no `--json` flag at all (confirmed live, it errors `Nonexistent flag: --json`) that can change or be pulled without notice; use the sibling `scripts/sf-rest.js` instead, which fetches the org's instanceUrl/access token itself and makes the callout directly with Node's built-in fetch:
114
+ ```bash
115
+ node ./scripts/sf-rest.js \
116
+ --target-org <username> --method POST \
117
+ --path "/services/apexrest/<NS_SEGMENT>measures/export/schedule" \
118
+ --body '{"metaname":"<internalName>","salesorg":"<salesOrgName>","object":"promotionmeasures"}'
119
+ ```
120
+ Capture `csvGuid` per Sales Org from the response — the schedule endpoint's response body field is actually named `requestId` (confirmed live), not `csvGuid`; use `requestId`'s value as the guid for Phase 8 and Phase 10 below. Dry-run ⇒ skip, record `pending — not run (dry-run)`.
121
+
122
+ ## Phase 8 — Poll export status (once per Sales Org from Phase 7)
123
+
124
+ Small dedicated loop (the response shape — a flat `Status` field, not `applicationStatus`/`apps[]` — doesn't match a generic app-install poller, so this doesn't reuse `consumer-goods-tpe-dashboard-configure`'s `poll-status.js`). Run once per `(csvGuid, salesOrgName)` pair captured in Phase 7. Via the sibling `scripts/sf-rest.js`, not `sf api request rest`:
125
+ ```bash
126
+ while true; do
127
+ status=$(node ./scripts/sf-rest.js \
128
+ --target-org <username> \
129
+ --path "/services/apexrest/<NS_SEGMENT>measures/export/<csvGuid>/status?salesorg=<salesOrgName>" \
130
+ | node -e "process.stdin.on('data',d=>console.log(JSON.parse(d).Status))")
131
+ echo "export status: $status"
132
+ case "$status" in
133
+ Ready) echo "export ready"; break ;;
134
+ Error|Aborted) echo "export failed: $status"; exit 1 ;;
135
+ Queued|InProgress) sleep 30 ;;
136
+ esac
137
+ done
138
+ ```
139
+ `Ready` = terminal success, `Error`/`Aborted` = terminal failure, `Queued`/`InProgress` = keep polling.
140
+
141
+ ## Phase 9 — Sync the `promotionmeasures` Data Stream
142
+
143
+ Phase 8's `Ready` status only confirms the export landed on the Processing services — the `promotionmeasures` Data Stream in Data Cloud still needs to ingest it before the exported measures show up downstream (KPI Sets, dashboards). This skill never triggers that sync itself — Data Stream sync/refresh isn't something this skill drives via `sf`/REST, and it's an action better left to the user to run interactively in Data Cloud setup.
144
+
145
+ **Run this once for the whole batch, not once per Sales Org** — the `promotionmeasures` Data Stream isn't scoped per Sales Org, so there's nothing to gain from asking after each one. Only ask once every Sales Org in `CONFIGURED_SALES_ORGS` has finished Phase 8's poll (whether `Ready` or otherwise recorded). This is a hard blocking gate, not a status update — pose it as an explicit yes/no confirmation question (the same way as the Phase 2 system-setting go-ahead and Phase 3 sync-trigger go-ahead), not as a plain narrated sentence that moves straight on to Phase 10: *"All exports have completed. Please go to Data Cloud → Data Streams → `promotionmeasures`, sync it, and wait until the sync is done (or wait until the scheduled sync completes). Has the sync finished?"* Do not proceed to Phase 10 on a generic "continue"/"next step" from the user — that answers a different question (moving the overall flow along), not this specific one (whether the Data Stream sync is actually done). Wait for an explicit confirmation before moving to Phase 10. Dry-run ⇒ skip, record `pending — not run (dry-run)`.
146
+
147
+ ## Phase 10 — Abort (only if the user asks to)
148
+
149
+ ```bash
150
+ node ./scripts/sf-rest.js \
151
+ --target-org <username> --method POST \
152
+ --path "/services/apexrest/<NS_SEGMENT>measures/export/abort/<csvGuid>?salesorg=<salesOrgName>&metaname=<internalName>"
153
+ ```
154
+ Never call this unless the user explicitly requests an abort.
155
+
156
+ ## Phase 11 — Report
157
+
158
+ Record pass/blocked/pending per phase (1-10 above) for the final report — this is exactly what `consumer-goods-tpe-dashboard-configure`'s corresponding step relays as its own result when this skill is invoked via delegation.
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Detects this org's namespace and derives every prefixed form this skill
4
+ * needs, replacing the manual "run sf, read the JSON, compute NS_* by hand"
5
+ * steps in SKILL.md's Namespace detection section. Same canonical procedure
6
+ * as `setup-tpe-dashboard`'s Namespace detection step — never re-derive a
7
+ * different way:
8
+ * 1. `sf package installed list` — find the "Consumer Goods Cloud" entry,
9
+ * NS = its SubscriberPackageNamespace.
10
+ * 2. Not found (source-deployed/dev-beta org) ⇒ `sf org display`'s
11
+ * `result.namespace`.
12
+ * 3. Still null/blank ⇒ NS = "" (genuinely unnamespaced org).
13
+ *
14
+ * CLI usage:
15
+ * node detect-namespace.js --target-org <username>
16
+ *
17
+ * Prints one line of JSON to stdout: {"NS","NS_SEGMENT","NS_APEX","NS_FIELD"}.
18
+ * Exit codes: 0 = success, 2 = usage/CLI error.
19
+ */
20
+
21
+ const { execFileSync } = require('child_process');
22
+
23
+ function sfJson(cliArgs) {
24
+ const raw = execFileSync('sf', cliArgs, { encoding: 'utf8' });
25
+ return JSON.parse(raw).result;
26
+ }
27
+
28
+ function detectNamespace(targetOrg) {
29
+ const packages = sfJson([
30
+ 'package', 'installed', 'list',
31
+ '--target-org', targetOrg, '--json',
32
+ ]);
33
+ const cgCloud = (packages || []).find(
34
+ (p) => p.SubscriberPackageName === 'Consumer Goods Cloud'
35
+ );
36
+ if (cgCloud && cgCloud.SubscriberPackageNamespace) {
37
+ return cgCloud.SubscriberPackageNamespace;
38
+ }
39
+
40
+ const { namespace } = sfJson([
41
+ 'org', 'display',
42
+ '--target-org', targetOrg, '--json',
43
+ ]);
44
+ return namespace || '';
45
+ }
46
+
47
+ function deriveForms(NS) {
48
+ return {
49
+ NS,
50
+ NS_SEGMENT: NS ? `${NS}/` : '',
51
+ NS_APEX: NS ? `${NS}.` : '',
52
+ NS_FIELD: NS ? `${NS}__` : '',
53
+ };
54
+ }
55
+
56
+ function parseArgs(argv) {
57
+ const args = {};
58
+ for (let i = 0; i < argv.length; i += 1) {
59
+ const token = argv[i];
60
+ if (!token.startsWith('--')) continue;
61
+ args[token.slice(2)] = argv[i + 1];
62
+ i += 1;
63
+ }
64
+ return args;
65
+ }
66
+
67
+ function requireArg(args, name) {
68
+ if (!args[name]) {
69
+ console.error(`Missing required --${name}`);
70
+ process.exit(2);
71
+ }
72
+ return args[name];
73
+ }
74
+
75
+ function main() {
76
+ const args = parseArgs(process.argv.slice(2));
77
+ const targetOrg = requireArg(args, 'target-org');
78
+ const NS = detectNamespace(targetOrg);
79
+ process.stdout.write(JSON.stringify(deriveForms(NS)));
80
+ }
81
+
82
+ if (require.main === module) {
83
+ main();
84
+ }
85
+
86
+ module.exports = { detectNamespace, deriveForms };
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Render one of this skill's `scripts/*.apex` templates by substituting
4
+ * `%TOKEN%` placeholders, writing the result to a temp file, and printing
5
+ * that file's path on stdout (so a caller can pipe it straight into
6
+ * `sf apex run -f <path>`).
7
+ *
8
+ * Usage:
9
+ * node render-apex.js <template-path> --var NS=cgcloud_dev__ --var NAME="My Set"
10
+ *
11
+ * Every `%TOKEN%` in the template must have a matching --var — this script
12
+ * fails loudly on any leftover `%TOKEN%` after substitution rather than
13
+ * silently sending unrendered Apex to the org.
14
+ *
15
+ * Every --var value is escaped (see soql-escape.js) before substitution, so
16
+ * callers must pass raw values — never pre-escape a value before handing it
17
+ * to this script, that would double-escape it.
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const os = require('os');
22
+ const path = require('path');
23
+ const { escapeSoqlLiteral } = require('./soql-escape');
24
+
25
+ function parseArgs(argv) {
26
+ const templatePath = argv[0];
27
+ const vars = {};
28
+ for (let i = 1; i < argv.length; i += 1) {
29
+ if (argv[i] !== '--var') continue;
30
+ const pair = argv[i + 1];
31
+ const eq = pair.indexOf('=');
32
+ vars[pair.slice(0, eq)] = pair.slice(eq + 1);
33
+ i += 1;
34
+ }
35
+ return { templatePath, vars };
36
+ }
37
+
38
+ function main() {
39
+ const { templatePath, vars } = parseArgs(process.argv.slice(2));
40
+ if (!templatePath) {
41
+ console.error('Missing required <template-path>');
42
+ process.exit(2);
43
+ }
44
+
45
+ let rendered = fs.readFileSync(templatePath, 'utf8');
46
+ for (const [key, value] of Object.entries(vars)) {
47
+ rendered = rendered.split(`%${key}%`).join(escapeSoqlLiteral(value));
48
+ }
49
+
50
+ const leftover = rendered.match(/%[A-Z_]+%/g);
51
+ if (leftover) {
52
+ console.error(`render-apex: unresolved placeholder(s): ${leftover.join(', ')}`);
53
+ process.exit(2);
54
+ }
55
+
56
+ const outPath = path.join(
57
+ os.tmpdir(),
58
+ `rtr-datacloud-${path.basename(templatePath, '.apex')}-${process.pid}.apex`
59
+ );
60
+ fs.writeFileSync(outPath, rendered, 'utf8');
61
+ console.log(outPath);
62
+ }
63
+
64
+ main();
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Resolve a record's Id by an exact-match Name query, safely — the Name
4
+ * value is escaped (see soql-escape.js) before being embedded in the SOQL
5
+ * literal, so a Name containing a quote (or a crafted injection payload)
6
+ * can't break out of it or inject additional clauses. Callers never build
7
+ * this WHERE clause by hand.
8
+ *
9
+ * Usage:
10
+ * node resolve-id-by-name.js --target-org <username> --object <ObjectApiName> --name "<value>"
11
+ *
12
+ * Prints the `sf data query --json` result verbatim to stdout.
13
+ */
14
+
15
+ const { execFileSync } = require('child_process');
16
+ const { escapeSoqlLiteral } = require('./soql-escape');
17
+
18
+ function parseArgs(argv) {
19
+ const args = {};
20
+ for (let i = 0; i < argv.length; i += 1) {
21
+ if (!argv[i].startsWith('--')) continue;
22
+ args[argv[i].slice(2)] = argv[i + 1];
23
+ i += 1;
24
+ }
25
+ return args;
26
+ }
27
+
28
+ function main() {
29
+ const args = parseArgs(process.argv.slice(2));
30
+ const targetOrg = args['target-org'];
31
+ const object = args['object'];
32
+ const name = args['name'];
33
+ if (!targetOrg || !object || name === undefined) {
34
+ console.error('Usage: resolve-id-by-name.js --target-org <username> --object <ObjectApiName> --name "<value>"');
35
+ process.exit(2);
36
+ }
37
+
38
+ const query = `SELECT Id, Name FROM ${object} WHERE Name = '${escapeSoqlLiteral(name)}'`;
39
+ const output = execFileSync(
40
+ 'sf',
41
+ ['data', 'query', '--target-org', targetOrg, '--json', '--query', query],
42
+ { encoding: 'utf8' }
43
+ );
44
+ process.stdout.write(output);
45
+ }
46
+
47
+ main();