@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.
- package/package.json +1 -1
- package/skills/consumer-goods-rtr-datacloud-export-configure/SKILL.md +72 -0
- package/skills/consumer-goods-rtr-datacloud-export-configure/references/inputs-and-namespace.md +38 -0
- package/skills/consumer-goods-rtr-datacloud-export-configure/references/procedure.md +158 -0
- package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/detect-namespace.js +86 -0
- package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/render-apex.js +64 -0
- package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/resolve-id-by-name.js +47 -0
- package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/sf-rest.js +171 -0
- package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/soql-escape.js +26 -0
- package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/upsert-report-config.apex +51 -0
- package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/upsert-system-setting.apex +21 -0
- package/skills/consumer-goods-tpe-dashboard-configure/SKILL.md +74 -0
- package/skills/consumer-goods-tpe-dashboard-configure/references/phases-1-6.md +112 -0
- package/skills/consumer-goods-tpe-dashboard-configure/references/phases-7-12.md +157 -0
- package/skills/consumer-goods-tpe-dashboard-configure/scripts/find-failure-reason.js +132 -0
- package/skills/consumer-goods-tpe-dashboard-configure/scripts/poll-status.js +116 -0
- package/skills/consumer-goods-tpe-dashboard-configure/scripts/render-apex.js +64 -0
- package/skills/consumer-goods-tpe-dashboard-configure/scripts/run-data-transform.js +121 -0
- package/skills/consumer-goods-tpe-dashboard-configure/scripts/schedule-business-period-export.apex +27 -0
- package/skills/consumer-goods-tpe-dashboard-configure/scripts/sf-rest.js +171 -0
- package/skills/consumer-goods-tpe-dashboard-configure/scripts/soql-escape.js +25 -0
- package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/SKILL.md +141 -0
- package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/references/payload-shapes.md +447 -0
- package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/references/procedure.md +263 -0
- package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/scripts/clone-tpe-dashboards.js +537 -0
- package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/scripts/sf-rest.js +195 -0
- package/skills/consumer-goods-tpe-datakit-deploy/SKILL.md +157 -0
- package/skills/consumer-goods-tpe-datakit-deploy/scripts/detect-namespace.js +86 -0
- package/skills/consumer-goods-tpe-datakit-deploy/scripts/download-static-resource.js +151 -0
- package/skills/consumer-goods-tpe-datakit-deploy/scripts/extract-crm-field-permissions.js +115 -0
- package/skills/consumer-goods-tpe-datakit-deploy/scripts/sf-rest.js +109 -0
- package/skills/consumer-goods-tpe-datakit-deploy/scripts/update-field-permissions.js +433 -0
- package/skills/experience-cms-content-generate/SKILL.md +2 -0
- package/skills/service-catalog-template-coordinate/SKILL.md +263 -0
- package/skills/service-catalog-template-coordinate/examples/output-templates.md +44 -0
- package/skills/service-catalog-template-coordinate/references/mcp-invocation.md +183 -0
- package/skills/service-catalog-template-coordinate/references/operations.md +230 -0
- package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/SKILL.md +243 -0
- package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/references/cli-invocation.md +205 -0
- package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/references/helper-contracts.md +236 -0
- package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/references/permset-topology.md +132 -0
- package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-activated-agents.mjs +106 -0
- package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-agent-access-state.mjs +113 -0
- package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-assignment-state.mjs +99 -0
- package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-platform-permset-availability.mjs +155 -0
- package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/gate-unified-catalog-tiers.mjs +100 -0
- package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/rank-candidate-users.mjs +95 -0
- package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/resolve-target-user.mjs +86 -0
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +44 -23
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/examples/output-templates.md +33 -9
- package/skills/service-itsm-agentic-setup-agentforce-studio-configure/SKILL.md +17 -18
- package/skills/service-itsm-agentic-setup-cmdb-coordinate/SKILL.md +3 -1
- package/skills/service-itsm-agentic-setup-configure/SKILL.md +20 -12
- package/skills/service-itsm-agentic-setup-configure/examples/output-templates.md +73 -5
- package/skills/service-itsm-agentic-setup-employee-agent-configure/SKILL.md +8 -7
- package/skills/service-itsm-agentic-setup-employee-agent-configure/references/cli-invocation.md +45 -33
- package/skills/service-itsm-agentic-setup-employee-agent-configure/references/reactivation.md +8 -6
- package/skills/service-itsm-agentic-setup-employee-agent-configure/references/workflow-detail.md +10 -10
- package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/classify-agent-existence.mjs +114 -56
- package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/classify-preflight.mjs +33 -17
- package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/render-report.mjs +9 -3
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/SKILL.md +8 -7
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/cli-invocation.md +43 -32
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/reactivation.md +6 -4
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/workflow-detail.md +10 -10
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-agent-existence.mjs +106 -55
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-preflight.mjs +27 -13
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/render-report.mjs +9 -3
- package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +159 -161
- package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone-action.json +51 -0
- package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone.json +1 -1
- package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/predefined-incident-policy.json +120 -0
- package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/milestone-patterns.md +28 -5
- package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/output-templates.md +19 -1
- package/skills/service-itsm-agentic-setup-incident-sla-configure/references/mcp-invocation.md +350 -30
- package/skills/service-itsm-channels-coordinate/SKILL.md +80 -213
- package/skills/service-itsm-slack-itservice-configure/SKILL.md +363 -0
- package/skills/service-itsm-slack-itservice-configure/references/connect-agentforce-to-slack.md +159 -0
- package/skills/service-itsm-slack-itservice-configure/references/manage-slack-connection.md +88 -0
- package/skills/service-itsm-slack-itservice-configure/references/manage-user-access.md +117 -0
- package/skills/service-itsm-slack-itservice-configure/references/record-visibility.md +78 -0
- package/skills/service-itsm-slack-itservice-configure/references/site-membership-verification.md +126 -0
- package/skills/service-itsm-slack-itservice-configure/scripts/classify-user-access.mjs +167 -0
- package/skills/service-itsm-teams-configure/SKILL.md +50 -47
- package/skills/service-itsm-teams-configure/references/azure-credential-population.md +42 -28
- package/skills/service-itsm-teams-configure/references/gotchas.md +1 -2
- package/skills/service-itsm-teams-coordinate/SKILL.md +22 -18
- package/skills/service-itsm-teams-coordinate/examples/output-templates.md +12 -9
- package/skills/service-itsm-teams-itdesk-configure/SKILL.md +60 -44
- package/skills/service-itsm-teams-itservice-configure/SKILL.md +56 -70
- package/skills/service-catalog-template-deploy/SKILL.md +0 -310
- package/skills/service-catalog-template-deploy/references/cli-invocation.md +0 -258
- package/skills/service-catalog-template-deploy/scripts/activate-verify.mjs +0 -164
- package/skills/service-catalog-template-deploy/scripts/build-deploy-payload.mjs +0 -94
- package/skills/service-catalog-template-deploy/scripts/resolve-template.mjs +0 -331
- package/skills/service-catalog-template-search/SKILL.md +0 -212
- package/skills/service-catalog-template-search/references/cli-invocation.md +0 -128
- 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. "'"
|
|
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(/'/g, "'")
|
|
74
|
+
.replace(/"/g, '"')
|
|
75
|
+
.replace(/&/g, '&')
|
|
76
|
+
.replace(/</g, '<')
|
|
77
|
+
.replace(/>/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
|
+
});
|
package/skills/consumer-goods-tpe-dashboard-configure/scripts/schedule-business-period-export.apex
ADDED
|
@@ -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 };
|