@salesforce/afv-skills 1.43.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 (98) 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/experience-cms-content-generate/SKILL.md +2 -0
  34. package/skills/service-catalog-template-coordinate/SKILL.md +263 -0
  35. package/skills/service-catalog-template-coordinate/examples/output-templates.md +44 -0
  36. package/skills/service-catalog-template-coordinate/references/mcp-invocation.md +183 -0
  37. package/skills/service-catalog-template-coordinate/references/operations.md +230 -0
  38. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/SKILL.md +243 -0
  39. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/references/cli-invocation.md +205 -0
  40. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/references/helper-contracts.md +236 -0
  41. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/references/permset-topology.md +132 -0
  42. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-activated-agents.mjs +106 -0
  43. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-agent-access-state.mjs +113 -0
  44. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-assignment-state.mjs +99 -0
  45. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-platform-permset-availability.mjs +155 -0
  46. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/gate-unified-catalog-tiers.mjs +100 -0
  47. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/rank-candidate-users.mjs +95 -0
  48. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/resolve-target-user.mjs +86 -0
  49. package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +44 -23
  50. package/skills/service-itsm-agentic-setup-agentforce-coordinate/examples/output-templates.md +33 -9
  51. package/skills/service-itsm-agentic-setup-agentforce-studio-configure/SKILL.md +17 -18
  52. package/skills/service-itsm-agentic-setup-cmdb-coordinate/SKILL.md +3 -1
  53. package/skills/service-itsm-agentic-setup-configure/SKILL.md +20 -12
  54. package/skills/service-itsm-agentic-setup-configure/examples/output-templates.md +73 -5
  55. package/skills/service-itsm-agentic-setup-employee-agent-configure/SKILL.md +8 -7
  56. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/cli-invocation.md +45 -33
  57. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/reactivation.md +8 -6
  58. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/workflow-detail.md +10 -10
  59. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/classify-agent-existence.mjs +114 -56
  60. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/classify-preflight.mjs +33 -17
  61. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/render-report.mjs +9 -3
  62. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/SKILL.md +8 -7
  63. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/cli-invocation.md +43 -32
  64. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/reactivation.md +6 -4
  65. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/workflow-detail.md +10 -10
  66. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-agent-existence.mjs +106 -55
  67. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-preflight.mjs +27 -13
  68. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/render-report.mjs +9 -3
  69. package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +159 -161
  70. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone-action.json +51 -0
  71. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone.json +1 -1
  72. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/predefined-incident-policy.json +120 -0
  73. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/milestone-patterns.md +28 -5
  74. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/output-templates.md +19 -1
  75. package/skills/service-itsm-agentic-setup-incident-sla-configure/references/mcp-invocation.md +350 -30
  76. package/skills/service-itsm-channels-coordinate/SKILL.md +80 -213
  77. package/skills/service-itsm-slack-itservice-configure/SKILL.md +363 -0
  78. package/skills/service-itsm-slack-itservice-configure/references/connect-agentforce-to-slack.md +159 -0
  79. package/skills/service-itsm-slack-itservice-configure/references/manage-slack-connection.md +88 -0
  80. package/skills/service-itsm-slack-itservice-configure/references/manage-user-access.md +117 -0
  81. package/skills/service-itsm-slack-itservice-configure/references/record-visibility.md +78 -0
  82. package/skills/service-itsm-slack-itservice-configure/references/site-membership-verification.md +126 -0
  83. package/skills/service-itsm-slack-itservice-configure/scripts/classify-user-access.mjs +167 -0
  84. package/skills/service-itsm-teams-configure/SKILL.md +50 -47
  85. package/skills/service-itsm-teams-configure/references/azure-credential-population.md +42 -28
  86. package/skills/service-itsm-teams-configure/references/gotchas.md +1 -2
  87. package/skills/service-itsm-teams-coordinate/SKILL.md +22 -18
  88. package/skills/service-itsm-teams-coordinate/examples/output-templates.md +12 -9
  89. package/skills/service-itsm-teams-itdesk-configure/SKILL.md +60 -44
  90. package/skills/service-itsm-teams-itservice-configure/SKILL.md +56 -70
  91. package/skills/service-catalog-template-deploy/SKILL.md +0 -310
  92. package/skills/service-catalog-template-deploy/references/cli-invocation.md +0 -258
  93. package/skills/service-catalog-template-deploy/scripts/activate-verify.mjs +0 -164
  94. package/skills/service-catalog-template-deploy/scripts/build-deploy-payload.mjs +0 -94
  95. package/skills/service-catalog-template-deploy/scripts/resolve-template.mjs +0 -331
  96. package/skills/service-catalog-template-search/SKILL.md +0 -212
  97. package/skills/service-catalog-template-search/references/cli-invocation.md +0 -128
  98. package/skills/service-catalog-template-search/scripts/classify-catalog.mjs +0 -205
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Extracts the actual failure reason for a FailedStatus app-framework install
4
+ * (e.g. the TPE Analytics App in Phase 11, or the C360 SDM app in Phase 6).
5
+ *
6
+ * `applicationStatus: FailedStatus` alone carries no detail — the underlying
7
+ * per-requirement failure message only lives on the install's Domino runtime
8
+ * (the App Framework's async execution graph), never on the app resource
9
+ * itself. This script walks that graph so nothing in this skill has to
10
+ * hand-parse it inline:
11
+ * 1. GET /app-framework/apps/<appId>/activities -> latest activity's
12
+ * runtimeRequest.id (the runtime request tied to the failed install).
13
+ * 2. GET /domino/runtimes/<runtimeRequestId> -> definition.nodes, each
14
+ * keyed by a graph node name (e.g. "template_requirement_<dmo>").
15
+ * 3. Collect every node whose results.validate.taskStatus starts with
16
+ * "Fail" (covers FailEndOfPhaseStatus and any other Fail* variant) and
17
+ * its statusMessage — that message is the human-readable reason.
18
+ * 4. Classify: a message matching the DMO-readiness template
19
+ * ("...[<dmo__dlm>] enabled or minimum required set of fields mapped
20
+ * in your org...") is a "DMO not available" failure; its DMO API name
21
+ * is pulled out of the message for the caller to act on (see this
22
+ * skill's Phase 11 debugging steps).
23
+ *
24
+ * Usage:
25
+ * node find-failure-reason.js --target-org <username> --app-id <appId>
26
+ *
27
+ * Prints one line of JSON: {"failedNodes":[{"node","statusMessage"}],
28
+ * "dmoNotAvailable":["ssot__BusinessPeriod__dlm", ...]}. Exit 0 whether or
29
+ * not a failure was found (this is a read-only diagnostic, not a
30
+ * pass/fail gate) — exit 1 only on a request/usage error.
31
+ */
32
+
33
+ const { restRequest } = require('./sf-rest');
34
+
35
+ function parseArgs(argv) {
36
+ const args = {};
37
+ for (let i = 0; i < argv.length; i += 1) {
38
+ const token = argv[i];
39
+ if (!token.startsWith('--')) continue;
40
+ args[token.slice(2)] = argv[i + 1];
41
+ i += 1;
42
+ }
43
+ return args;
44
+ }
45
+
46
+ function requireArg(args, name) {
47
+ if (!args[name]) {
48
+ console.error(`Missing required --${name}`);
49
+ process.exit(1);
50
+ }
51
+ return args[name];
52
+ }
53
+
54
+ async function getJson(targetOrg, path) {
55
+ const { ok, status, body } = await restRequest({ targetOrg, path, method: 'GET' });
56
+ if (!ok) {
57
+ throw new Error(`HTTP ${status} for ${path}: ${JSON.stringify(body)}`);
58
+ }
59
+ return body;
60
+ }
61
+
62
+ // The chain's own failMessage template is inconsistent about the opening
63
+ // "[" (confirmed live: BusinessPeriod's message has it, MeasureDefinition's
64
+ // doesn't) — match the closing "]" only and treat the opening one as
65
+ // optional. statusMessage also arrives HTML-entity-encoded (e.g. "&#39;"
66
+ // for an apostrophe); decode the handful of entities this template uses
67
+ // before matching or displaying it.
68
+ const DMO_NOT_AVAILABLE_PATTERN =
69
+ /don't have \[?([\w.]+__dlm)\] enabled or minimum required set of fields mapped/;
70
+
71
+ function decodeEntities(text) {
72
+ return text
73
+ .replace(/&#39;/g, "'")
74
+ .replace(/&quot;/g, '"')
75
+ .replace(/&amp;/g, '&')
76
+ .replace(/&lt;/g, '<')
77
+ .replace(/&gt;/g, '>');
78
+ }
79
+
80
+ async function main() {
81
+ const args = parseArgs(process.argv.slice(2));
82
+ const targetOrg = requireArg(args, 'target-org');
83
+ const appId = requireArg(args, 'app-id');
84
+
85
+ let runtimeRequestId = args['runtime-request-id'];
86
+ if (!runtimeRequestId) {
87
+ const activities = await getJson(
88
+ targetOrg,
89
+ `/services/data/v67.0/app-framework/apps/${appId}/activities`
90
+ );
91
+ const items = activities.items || [];
92
+ if (!items.length) {
93
+ process.stdout.write(JSON.stringify({ failedNodes: [], dmoNotAvailable: [] }));
94
+ return;
95
+ }
96
+ // Activities are returned most-recent-first; the latest install attempt
97
+ // is the one whose reason we want.
98
+ runtimeRequestId = items[0].runtimeRequest && items[0].runtimeRequest.id;
99
+ if (!runtimeRequestId) {
100
+ process.stdout.write(JSON.stringify({ failedNodes: [], dmoNotAvailable: [] }));
101
+ return;
102
+ }
103
+ }
104
+
105
+ const runtime = await getJson(
106
+ targetOrg,
107
+ `/services/data/v67.0/domino/runtimes/${runtimeRequestId}`
108
+ );
109
+ const nodes = (runtime.definition && runtime.definition.nodes) || {};
110
+
111
+ const failedNodes = [];
112
+ const dmoNotAvailable = [];
113
+
114
+ for (const [nodeName, node] of Object.entries(nodes)) {
115
+ const validate = node.results && node.results.validate;
116
+ const taskStatus = validate && validate.taskStatus;
117
+ if (!taskStatus || !taskStatus.startsWith('Fail')) continue;
118
+
119
+ const statusMessage = decodeEntities(validate.statusMessage || '');
120
+ failedNodes.push({ node: nodeName, statusMessage });
121
+
122
+ const match = statusMessage.match(DMO_NOT_AVAILABLE_PATTERN);
123
+ if (match) dmoNotAvailable.push(match[1]);
124
+ }
125
+
126
+ process.stdout.write(JSON.stringify({ failedNodes, dmoNotAvailable }));
127
+ }
128
+
129
+ main().catch((err) => {
130
+ console.error(err);
131
+ process.exit(1);
132
+ });
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generic status poller for async App Framework installs (and any other
4
+ * REST endpoint shaped the same way).
5
+ *
6
+ * Usage:
7
+ * node poll-status.js \
8
+ * --target-org <username> \
9
+ * --path "/services/data/v67.0/app-framework/apps/<appId>" \
10
+ * --status-field applicationStatus \
11
+ * --success-values SuccessStatus,SuccessWithWarningsStatus \
12
+ * --failure-values FailedStatus \
13
+ * --interval-seconds 15 \
14
+ * --max-wait-seconds 300
15
+ *
16
+ * Behavior:
17
+ * - Calls `restRequest()` from the sibling `sf-rest.js` on every tick — a
18
+ * direct authenticated HTTP GET, not `sf api request rest` (beta, no
19
+ * `--json`, can change or be pulled without notice).
20
+ * - Reads --status-field off the parsed response body. Handles both a
21
+ * single-object response and a first-array-element response (same
22
+ * shape covers `apps/{id}` and `apps?templateSourceId=...` list calls).
23
+ * - Exit 0 on a success value, exit 1 on a failure value, exit 2 on
24
+ * timeout (still not-terminal) — distinct so a caller can tell
25
+ * "still installing" apart from "actually failed".
26
+ *
27
+ * Reused as-is by every async-install step in this skill — do not write a
28
+ * second poll loop.
29
+ */
30
+
31
+ const { restRequest } = require('./sf-rest');
32
+
33
+ function parseArgs(argv) {
34
+ const args = {};
35
+ for (let i = 0; i < argv.length; i += 1) {
36
+ const token = argv[i];
37
+ if (!token.startsWith('--')) continue;
38
+ const key = token.slice(2);
39
+ const value = argv[i + 1];
40
+ args[key] = value;
41
+ i += 1;
42
+ }
43
+ return args;
44
+ }
45
+
46
+ function requireArg(args, name) {
47
+ if (!args[name]) {
48
+ console.error(`Missing required --${name}`);
49
+ process.exit(2);
50
+ }
51
+ return args[name];
52
+ }
53
+
54
+ async function fetchStatus(targetOrg, path) {
55
+ const { ok, status, body } = await restRequest({ targetOrg, path, method: 'GET' });
56
+ if (!ok) {
57
+ throw new Error(`HTTP ${status}: ${JSON.stringify(body)}`);
58
+ }
59
+ if (Array.isArray(body)) {
60
+ return body[0];
61
+ }
62
+ if (body && Array.isArray(body.apps)) {
63
+ return body.apps[0];
64
+ }
65
+ return body;
66
+ }
67
+
68
+ function sleep(ms) {
69
+ return new Promise((resolve) => setTimeout(resolve, ms));
70
+ }
71
+
72
+ async function main() {
73
+ const args = parseArgs(process.argv.slice(2));
74
+ const targetOrg = requireArg(args, 'target-org');
75
+ const path = requireArg(args, 'path');
76
+ const statusField = requireArg(args, 'status-field');
77
+ const successValues = requireArg(args, 'success-values').split(',');
78
+ const failureValues = requireArg(args, 'failure-values').split(',');
79
+ const intervalSeconds = Number(args['interval-seconds'] || '15');
80
+ const maxWaitSeconds = Number(args['max-wait-seconds'] || '300');
81
+
82
+ const deadline = Date.now() + maxWaitSeconds * 1000;
83
+
84
+ for (;;) {
85
+ let record;
86
+ try {
87
+ record = await fetchStatus(targetOrg, path);
88
+ } catch (err) {
89
+ console.error(`poll-status: request failed: ${err.message}`);
90
+ process.exit(1);
91
+ }
92
+
93
+ const status = record ? record[statusField] : undefined;
94
+ console.log(`poll-status: ${statusField}=${status}`);
95
+
96
+ if (successValues.includes(status)) {
97
+ console.log(`poll-status: terminal success (${status})`);
98
+ process.exit(0);
99
+ }
100
+ if (failureValues.includes(status)) {
101
+ console.error(`poll-status: terminal failure (${status})`);
102
+ process.exit(1);
103
+ }
104
+
105
+ if (Date.now() >= deadline) {
106
+ console.error(
107
+ `poll-status: timed out after ${maxWaitSeconds}s, last status=${status}`
108
+ );
109
+ process.exit(2);
110
+ }
111
+
112
+ await sleep(intervalSeconds * 1000);
113
+ }
114
+ }
115
+
116
+ main();
@@ -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_FIELD=cgcloud_dev__ --var NS_APEX=cgcloud_dev.
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
+ `tpe-dashboard-${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,121 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Trigger and poll a Data Cloud (SSOT) Data Transform via the sibling
4
+ * `sf-rest.js`'s authenticated HTTP callout, reimplementing the run+wait
5
+ * contract confirmed against this repo's own
6
+ * `packages/liability-accruals/test/cdp-client.js`
7
+ * (`waitForTransform` / `runAndWaitForTransform`) — that file is grounding
8
+ * evidence for the REST contract only, never a runtime dependency; this
9
+ * script never shells out to it or assumes `packages/liability-accruals`
10
+ * is present in the checkout.
11
+ *
12
+ * Usage:
13
+ * node run-data-transform.js \
14
+ * --target-org <username> \
15
+ * --transform-name TPM_PROMOTION_MEASURE \
16
+ * [--api-version v67.0] \
17
+ * [--poll-interval-seconds 60] \
18
+ * [--max-wait-seconds 3600]
19
+ *
20
+ * Exit codes: 0 = terminal success, 1 = terminal failure (FAILURE with a
21
+ * fresh lastRunDate), 2 = timeout while still pending/in-progress.
22
+ *
23
+ * Auth: the org's plain Salesforce access token via the already-
24
+ * authenticated `sf` CLI user is sufficient for all three calls below —
25
+ * despite the `/ssot/` path segment, this is not the separate CDP/A360
26
+ * token exchange `cdp-client.js` performs for its unrelated ingestion
27
+ * calls.
28
+ */
29
+
30
+ const { restRequest } = require('./sf-rest');
31
+
32
+ function parseArgs(argv) {
33
+ const args = {};
34
+ for (let i = 0; i < argv.length; i += 1) {
35
+ const token = argv[i];
36
+ if (!token.startsWith('--')) continue;
37
+ args[token.slice(2)] = argv[i + 1];
38
+ i += 1;
39
+ }
40
+ return args;
41
+ }
42
+
43
+ function requireArg(args, name) {
44
+ if (!args[name]) {
45
+ console.error(`Missing required --${name}`);
46
+ process.exit(2);
47
+ }
48
+ return args[name];
49
+ }
50
+
51
+ async function sfApi(targetOrg, path, method) {
52
+ // A direct authenticated HTTP call via the sibling `sf-rest.js`, not
53
+ // `sf api request rest` — that command is beta, has no `--json` flag at
54
+ // all (confirmed live — it errors "Nonexistent flag: --json"), and beta
55
+ // CLI surfaces can change or be pulled without notice.
56
+ const { ok, status, body } = await restRequest({ targetOrg, path, method });
57
+ if (!ok) {
58
+ throw new Error(`HTTP ${status}: ${JSON.stringify(body)}`);
59
+ }
60
+ return body;
61
+ }
62
+
63
+ function sleep(ms) {
64
+ return new Promise((resolve) => setTimeout(resolve, ms));
65
+ }
66
+
67
+ function transformPath(apiVersion, name) {
68
+ return `/services/data/${apiVersion}/ssot/data-transforms/${name}`;
69
+ }
70
+
71
+ async function main() {
72
+ const args = parseArgs(process.argv.slice(2));
73
+ const targetOrg = requireArg(args, 'target-org');
74
+ const name = requireArg(args, 'transform-name');
75
+ const apiVersion = args['api-version'] || 'v67.0';
76
+ const pollIntervalSeconds = Number(args['poll-interval-seconds'] || '60');
77
+ const maxWaitSeconds = Number(args['max-wait-seconds'] || String(1 * 60 * 60));
78
+
79
+ const path = transformPath(apiVersion, name);
80
+ const baseline = await sfApi(targetOrg, path, 'GET');
81
+ const baselineRunDate = baseline ? baseline.lastRunDate : null;
82
+ console.log(`run-data-transform: name=${name}, baselineRunDate=${baselineRunDate}`);
83
+
84
+ await sfApi(targetOrg, `${path}/actions/run`, 'POST');
85
+ console.log('run-data-transform: run triggered');
86
+
87
+ const deadline = Date.now() + maxWaitSeconds * 1000;
88
+
89
+ for (;;) {
90
+ await sfApi(targetOrg, `${path}/actions/refresh-status`, 'POST');
91
+ const current = await sfApi(targetOrg, path, 'GET');
92
+ const { lastRunStatus, lastRunDate, lastRunErrorMessage } = current;
93
+ console.log(`run-data-transform: lastRunStatus=${lastRunStatus} lastRunDate=${lastRunDate}`);
94
+
95
+ const isFresh = lastRunDate !== baselineRunDate;
96
+ const isPending = lastRunStatus === 'PENDING' || lastRunStatus === 'IN_PROGRESS';
97
+
98
+ if (isFresh && !isPending) {
99
+ if (lastRunStatus === 'FAILURE') {
100
+ console.error(`run-data-transform: FAILURE — ${lastRunErrorMessage}`);
101
+ process.exit(1);
102
+ }
103
+ console.log(`run-data-transform: terminal success (${lastRunStatus})`);
104
+ process.exit(0);
105
+ }
106
+
107
+ if (Date.now() >= deadline) {
108
+ console.error(
109
+ `run-data-transform: timed out after ${maxWaitSeconds}s, last status=${lastRunStatus} (still pending — not a failure)`
110
+ );
111
+ process.exit(2);
112
+ }
113
+
114
+ await sleep(pollIntervalSeconds * 1000);
115
+ }
116
+ }
117
+
118
+ main().catch((err) => {
119
+ console.error(err);
120
+ process.exit(1);
121
+ });
@@ -0,0 +1,27 @@
1
+ // Triggers SCHEDULE_RTR_EXPORT for the tpmbusinessperiod object, across every
2
+ // Sales Org ("salesorg":"*" in the JSON body — a business-level "export for
3
+ // every sales org" instruction, not a platform-level wildcard).
4
+ //
5
+ // This must stay anonymous Apex, not a direct REST call to
6
+ // ScheduleRTRExportService (/measures/export/schedule): that REST resource
7
+ // reads a single "salesorg" field out of its JSON body and uses that same
8
+ // value for BOTH tenant/substrate routing (SalesOrgSubstrate mode —
9
+ // SalesOrganizationServiceWOSharing.getTenantSubstrateEnum(salesorg)) and the
10
+ // forwarded payload. "*" doesn't resolve to a real Sales_Organization__c, so
11
+ // it can't be used for routing. This template decouples the two: %SALES_ORG%
12
+ // (a real Hyperforce-substrate Sales Org Name) drives routing via the
13
+ // OffPlatformCallout constructor; the JSON body's "salesorg" stays "*".
14
+ //
15
+ // Placeholders (each substituted exactly once, into a String variable
16
+ // declaration below — render-apex.js escapes it there; see soql-escape.js):
17
+ // %NS_APEX% -> Apex class-reference prefix, e.g. "cgcloud_dev." (empty for unnamespaced)
18
+ // %SALES_ORG% -> Name of a real Hyperforce-substrate Sales_Organization__c, resolved by the caller (step 1's `sf data query`)
19
+ String salesOrg = '%SALES_ORG%';
20
+ Map<String, Object> data = new Map<String, Object>();
21
+ data.put('salesorg', '*');
22
+ data.put('object', 'tpmbusinessperiod');
23
+ %NS_APEX%OffPlatformCallout request = new %NS_APEX%OffPlatformCallout('SCHEDULE_RTR_EXPORT', salesOrg);
24
+ %NS_APEX%OffPlatformCalloutResponse response = request.execute(
25
+ %NS_APEX%TransactionHandler.getTransactionIdentifier(), null, JSON.serialize(data, true)
26
+ );
27
+ System.debug(LoggingLevel.INFO, 'SCHEDULE_RTR_EXPORT_RESPONSE=' + response);
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Authenticated Salesforce REST callout, replacing `sf api request rest`
4
+ * everywhere in this skill — that command is beta, doesn't support --json,
5
+ * and beta CLI surfaces can change or be pulled without notice. Fetches the
6
+ * org's instanceUrl and access token via the `sf` CLI, then performs a
7
+ * plain HTTP request with Node's built-in fetch. Access token retrieval is
8
+ * version-gated the same way as `datakit-for-tpe`'s
9
+ * `download-static-resource.js`: `sf org auth show-access-token` for CLI
10
+ * >= 2.136.8 (below that version, `sf org display`'s own `result.accessToken`
11
+ * is a redacted hint string, not a real token — see the `sfJson`/`getAccessToken`
12
+ * pair below). The token is never printed.
13
+ *
14
+ * CLI usage:
15
+ * node sf-rest.js --target-org <username> --path <path> \
16
+ * [--method GET|POST|PATCH|DELETE] [--body <json-string>]
17
+ *
18
+ * <path> is relative to the org's instanceUrl, e.g.
19
+ * "/services/apexrest/tpm-api/..." or "/services/data/v67.0/...".
20
+ * Prints the response body as one line of JSON to stdout on both success
21
+ * and failure, so a non-2xx error body (e.g. `{"error":"..."}`) is still
22
+ * readable by the caller. Exit codes: 0 = 2xx response, 1 = non-2xx
23
+ * response, 2 = usage/CLI error.
24
+ *
25
+ * Module usage (for sibling scripts in this same skill's scripts/ dir —
26
+ * never imported across skill directories):
27
+ * const { restRequest } = require('./sf-rest');
28
+ * const { ok, status, body } = await restRequest({ targetOrg, path, method, body: jsonString });
29
+ */
30
+
31
+ /**
32
+ * Also exposes two subcommands that print only safe, non-secret fields from
33
+ * `sf org display --json` — that command's `result` includes `accessToken`
34
+ * (a live credential), so nothing in this skill should run it directly and
35
+ * surface the raw output; these strip it before printing:
36
+ * node sf-rest.js org-status --target-org <username>
37
+ * -> {"username","alias","connectedStatus","orgId"} ; exit 1 if not Connected
38
+ */
39
+
40
+ const { execFileSync } = require('child_process');
41
+
42
+ function sfJson(cliArgs) {
43
+ const raw = execFileSync('sf', cliArgs, { encoding: 'utf8' });
44
+ return JSON.parse(raw).result;
45
+ }
46
+
47
+ const ACCESS_TOKEN_VIA_SHOW_COMMAND_SINCE = [2, 136, 8];
48
+
49
+ function getCliVersion() {
50
+ // Unlike every other `sf ... --json` command, `sf version --json` returns
51
+ // its fields at the top level — no `{status, result}` envelope.
52
+ const raw = execFileSync('sf', ['version', '--json'], { encoding: 'utf8' });
53
+ const { cliVersion } = JSON.parse(raw);
54
+ const match = cliVersion.match(/(\d+)\.(\d+)\.(\d+)/);
55
+ // A pre-release/suffixed version string (e.g. a nightly build) that
56
+ // doesn't match this pattern is treated as "new enough" below, via
57
+ // isAtLeast's null passthrough — never silently fall back to the
58
+ // redacted-token path on an unparseable version.
59
+ return match ? match.slice(1).map(Number) : null;
60
+ }
61
+
62
+ function isAtLeast(version, minVersion) {
63
+ if (!version) return true;
64
+ for (let i = 0; i < minVersion.length; i += 1) {
65
+ const a = version[i] || 0;
66
+ const b = minVersion[i] || 0;
67
+ if (a !== b) return a > b;
68
+ }
69
+ return true;
70
+ }
71
+
72
+ function getAccessToken(targetOrg) {
73
+ const useShowAccessTokenCommand = isAtLeast(
74
+ getCliVersion(),
75
+ ACCESS_TOKEN_VIA_SHOW_COMMAND_SINCE
76
+ );
77
+ if (useShowAccessTokenCommand) {
78
+ return sfJson([
79
+ 'org', 'auth', 'show-access-token',
80
+ '--target-org', targetOrg,
81
+ '--no-prompt', '--json',
82
+ ]).accessToken;
83
+ }
84
+ return sfJson(['org', 'display', '--target-org', targetOrg, '--json']).accessToken;
85
+ }
86
+
87
+ async function restRequest({ targetOrg, path, method, body }) {
88
+ const { instanceUrl } = sfJson(['org', 'display', '--target-org', targetOrg, '--json']);
89
+ const accessToken = getAccessToken(targetOrg);
90
+
91
+ const headers = { Authorization: `Bearer ${accessToken}` };
92
+ if (body !== undefined) headers['Content-Type'] = 'application/json';
93
+
94
+ const response = await fetch(`${instanceUrl}${path}`, {
95
+ method: method || 'GET',
96
+ headers,
97
+ body,
98
+ });
99
+ const text = await response.text();
100
+ let parsedBody = {};
101
+ if (text.length) {
102
+ try {
103
+ parsedBody = JSON.parse(text);
104
+ } catch {
105
+ parsedBody = text;
106
+ }
107
+ }
108
+ return { ok: response.ok, status: response.status, body: parsedBody };
109
+ }
110
+
111
+ function getOrgStatus(targetOrg) {
112
+ const info = sfJson(['org', 'display', '--target-org', targetOrg, '--json']);
113
+ return {
114
+ username: info.username,
115
+ alias: info.alias || null,
116
+ connectedStatus: info.connectedStatus,
117
+ orgId: info.id,
118
+ };
119
+ }
120
+
121
+ function parseArgs(argv) {
122
+ const args = {};
123
+ for (let i = 0; i < argv.length; i += 1) {
124
+ const token = argv[i];
125
+ if (!token.startsWith('--')) continue;
126
+ args[token.slice(2)] = argv[i + 1];
127
+ i += 1;
128
+ }
129
+ return args;
130
+ }
131
+
132
+ function requireArg(args, name) {
133
+ if (!args[name]) {
134
+ console.error(`Missing required --${name}`);
135
+ process.exit(2);
136
+ }
137
+ return args[name];
138
+ }
139
+
140
+ async function main() {
141
+ const [maybeCommand, ...rest] = process.argv.slice(2);
142
+
143
+ if (maybeCommand === 'org-status') {
144
+ const args = parseArgs(rest);
145
+ const status = getOrgStatus(requireArg(args, 'target-org'));
146
+ process.stdout.write(JSON.stringify(status));
147
+ if (status.connectedStatus !== 'Connected') process.exit(1);
148
+ return;
149
+ }
150
+
151
+ const args = parseArgs(process.argv.slice(2));
152
+ const targetOrg = requireArg(args, 'target-org');
153
+ const path = requireArg(args, 'path');
154
+ const method = args.method || 'GET';
155
+
156
+ const { ok, status, body } = await restRequest({ targetOrg, path, method, body: args.body });
157
+ process.stdout.write(JSON.stringify(body));
158
+ if (!ok) {
159
+ console.error(`sf-rest: HTTP ${status}`);
160
+ process.exit(1);
161
+ }
162
+ }
163
+
164
+ if (require.main === module) {
165
+ main().catch((err) => {
166
+ console.error(err);
167
+ process.exit(1);
168
+ });
169
+ }
170
+
171
+ module.exports = { restRequest, getOrgStatus };
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Escape a value for safe interpolation into a single-quoted SOQL/Apex
4
+ * string literal — escape backslashes first, then single quotes, so a value
5
+ * like `O'Brien Foods` (or one ending in a backslash) can't break out of the
6
+ * literal it's placed in. Used by render-apex.js (Apex template
7
+ * substitution).
8
+ *
9
+ * CLI usage: node scripts/soql-escape.js "O'Brien Foods"
10
+ */
11
+
12
+ function escapeSoqlLiteral(value) {
13
+ return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
14
+ }
15
+
16
+ if (require.main === module) {
17
+ const value = process.argv[2];
18
+ if (value === undefined) {
19
+ console.error('Usage: soql-escape.js <value>');
20
+ process.exit(2);
21
+ }
22
+ console.log(escapeSoqlLiteral(value));
23
+ }
24
+
25
+ module.exports = { escapeSoqlLiteral };