@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.
- 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/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,195 @@
|
|
|
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/v63.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
|
+
// A color-forcing terminal (e.g. Warp's FORCE_COLOR=3) makes `sf` emit ANSI
|
|
44
|
+
// codes even with --json, which breaks JSON.parse — force color off for
|
|
45
|
+
// this invocation regardless of the calling shell's env.
|
|
46
|
+
const raw = execFileSync('sf', cliArgs, {
|
|
47
|
+
encoding: 'utf8',
|
|
48
|
+
env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' },
|
|
49
|
+
});
|
|
50
|
+
return JSON.parse(raw).result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const ACCESS_TOKEN_VIA_SHOW_COMMAND_SINCE = [2, 136, 8];
|
|
54
|
+
|
|
55
|
+
function getCliVersion() {
|
|
56
|
+
// Unlike every other `sf ... --json` command, `sf version --json` returns
|
|
57
|
+
// its fields at the top level — no `{status, result}` envelope.
|
|
58
|
+
const raw = execFileSync('sf', ['version', '--json'], {
|
|
59
|
+
encoding: 'utf8',
|
|
60
|
+
env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' },
|
|
61
|
+
});
|
|
62
|
+
const { cliVersion } = JSON.parse(raw);
|
|
63
|
+
const match = cliVersion.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
64
|
+
// A pre-release/suffixed version string (e.g. a nightly build) that
|
|
65
|
+
// doesn't match this pattern is treated as "new enough" below, via
|
|
66
|
+
// isAtLeast's null passthrough — never silently fall back to the
|
|
67
|
+
// redacted-token path on an unparseable version.
|
|
68
|
+
return match ? match.slice(1).map(Number) : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isAtLeast(version, minVersion) {
|
|
72
|
+
if (!version) return true;
|
|
73
|
+
for (let i = 0; i < minVersion.length; i += 1) {
|
|
74
|
+
const a = version[i] || 0;
|
|
75
|
+
const b = minVersion[i] || 0;
|
|
76
|
+
if (a !== b) return a > b;
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getAccessToken(targetOrg) {
|
|
82
|
+
const useShowAccessTokenCommand = isAtLeast(
|
|
83
|
+
getCliVersion(),
|
|
84
|
+
ACCESS_TOKEN_VIA_SHOW_COMMAND_SINCE
|
|
85
|
+
);
|
|
86
|
+
if (useShowAccessTokenCommand) {
|
|
87
|
+
return sfJson([
|
|
88
|
+
'org', 'auth', 'show-access-token',
|
|
89
|
+
'--target-org', targetOrg,
|
|
90
|
+
'--no-prompt', '--json',
|
|
91
|
+
]).accessToken;
|
|
92
|
+
}
|
|
93
|
+
return sfJson(['org', 'display', '--target-org', targetOrg, '--json']).accessToken;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// instanceUrl/accessToken don't change within a single script run — caching
|
|
97
|
+
// them per targetOrg avoids 3 fresh `sf` CLI subprocess spawns (org display,
|
|
98
|
+
// version, show-access-token) on every single REST call, which otherwise
|
|
99
|
+
// dominates runtime once a script makes dozens of calls (e.g. cloning every
|
|
100
|
+
// KPI-bound visualization on a dashboard).
|
|
101
|
+
const orgConnectionCache = new Map();
|
|
102
|
+
|
|
103
|
+
function getOrgConnection(targetOrg) {
|
|
104
|
+
if (!orgConnectionCache.has(targetOrg)) {
|
|
105
|
+
const { instanceUrl } = sfJson(['org', 'display', '--target-org', targetOrg, '--json']);
|
|
106
|
+
const accessToken = getAccessToken(targetOrg);
|
|
107
|
+
orgConnectionCache.set(targetOrg, { instanceUrl, accessToken });
|
|
108
|
+
}
|
|
109
|
+
return orgConnectionCache.get(targetOrg);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function restRequest({ targetOrg, path, method, body }) {
|
|
113
|
+
const { instanceUrl, accessToken } = getOrgConnection(targetOrg);
|
|
114
|
+
|
|
115
|
+
const headers = { Authorization: `Bearer ${accessToken}` };
|
|
116
|
+
if (body !== undefined) headers['Content-Type'] = 'application/json';
|
|
117
|
+
|
|
118
|
+
const response = await fetch(`${instanceUrl}${path}`, {
|
|
119
|
+
method: method || 'GET',
|
|
120
|
+
headers,
|
|
121
|
+
body,
|
|
122
|
+
});
|
|
123
|
+
const text = await response.text();
|
|
124
|
+
let parsedBody = {};
|
|
125
|
+
if (text.length) {
|
|
126
|
+
try {
|
|
127
|
+
parsedBody = JSON.parse(text);
|
|
128
|
+
} catch {
|
|
129
|
+
parsedBody = text;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return { ok: response.ok, status: response.status, body: parsedBody };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getOrgStatus(targetOrg) {
|
|
136
|
+
const info = sfJson(['org', 'display', '--target-org', targetOrg, '--json']);
|
|
137
|
+
return {
|
|
138
|
+
username: info.username,
|
|
139
|
+
alias: info.alias || null,
|
|
140
|
+
connectedStatus: info.connectedStatus,
|
|
141
|
+
orgId: info.id,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function parseArgs(argv) {
|
|
146
|
+
const args = {};
|
|
147
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
148
|
+
const token = argv[i];
|
|
149
|
+
if (!token.startsWith('--')) continue;
|
|
150
|
+
args[token.slice(2)] = argv[i + 1];
|
|
151
|
+
i += 1;
|
|
152
|
+
}
|
|
153
|
+
return args;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function requireArg(args, name) {
|
|
157
|
+
if (!args[name]) {
|
|
158
|
+
console.error(`Missing required --${name}`);
|
|
159
|
+
process.exit(2);
|
|
160
|
+
}
|
|
161
|
+
return args[name];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function main() {
|
|
165
|
+
const [maybeCommand, ...rest] = process.argv.slice(2);
|
|
166
|
+
|
|
167
|
+
if (maybeCommand === 'org-status') {
|
|
168
|
+
const args = parseArgs(rest);
|
|
169
|
+
const status = getOrgStatus(requireArg(args, 'target-org'));
|
|
170
|
+
process.stdout.write(JSON.stringify(status));
|
|
171
|
+
if (status.connectedStatus !== 'Connected') process.exit(1);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const args = parseArgs(process.argv.slice(2));
|
|
176
|
+
const targetOrg = requireArg(args, 'target-org');
|
|
177
|
+
const path = requireArg(args, 'path');
|
|
178
|
+
const method = args.method || 'GET';
|
|
179
|
+
|
|
180
|
+
const { ok, status, body } = await restRequest({ targetOrg, path, method, body: args.body });
|
|
181
|
+
process.stdout.write(JSON.stringify(body));
|
|
182
|
+
if (!ok) {
|
|
183
|
+
console.error(`sf-rest: HTTP ${status}`);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (require.main === module) {
|
|
189
|
+
main().catch((err) => {
|
|
190
|
+
console.error(err);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = { restRequest, getOrgStatus };
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: consumer-goods-tpe-datakit-deploy
|
|
3
|
+
description: "Deploy the TPM Accruals Data Kit components specifically required for Trade Promotion Effectiveness (TPE) dashboards. Use when a customer or admin wants to install or deploy the TPM data kit for TPE dashboards, or as a delegated step from consumer-goods-tpe-dashboard-configure. Do not use this skill if the customer wants to deploy the TPM Accruals Data Kit for the Accruals feature."
|
|
4
|
+
metadata:
|
|
5
|
+
version: "1.0"
|
|
6
|
+
minApiVersion: "67.0"
|
|
7
|
+
domains: ["Consumer Goods"]
|
|
8
|
+
relatedSkills:
|
|
9
|
+
- "consumer-goods-tpe-dashboard-configure"
|
|
10
|
+
cliTools:
|
|
11
|
+
- tool: ["node"]
|
|
12
|
+
semver: ">=20.0.0"
|
|
13
|
+
- tool: ["sf"]
|
|
14
|
+
semver: ">=2.0.0"
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# TPE Data Kit Deploy
|
|
18
|
+
|
|
19
|
+
Deploys the TPM Accruals Data Kit and its DMO mappings into a Salesforce org. Standalone and self-contained — runs on its own, or as a delegated step invoked by `consumer-goods-tpe-dashboard-configure` (Step 7), which passes through its own already-collected username and dry-run flag rather than this skill re-deriving them.
|
|
20
|
+
|
|
21
|
+
## Inputs to collect first
|
|
22
|
+
|
|
23
|
+
Ask before starting, unless already supplied by a calling skill. Do not guess.
|
|
24
|
+
|
|
25
|
+
1. **TPM System Admin username** — already authenticated via `sf` CLI. Verify via the sibling
|
|
26
|
+
script, never `sf org display --json` directly — that command's raw output includes
|
|
27
|
+
`accessToken` (a live credential), which this script strips before printing:
|
|
28
|
+
```bash
|
|
29
|
+
node ./scripts/sf-rest.js org-status --target-org <username>
|
|
30
|
+
```
|
|
31
|
+
Prints `{"username","alias","connectedStatus","orgId"}`. Non-zero exit or
|
|
32
|
+
`connectedStatus != "Connected"` ⇒ stop, ask the user to log in themselves.
|
|
33
|
+
2. **Dry-run?** — offer by default. In dry-run, Phase 1 (preflight) and Phase 2's read/diff steps still run; Phase 2's `FieldPermissions` writes and Phases 3-5 (deploy) are skipped and recorded as `pending — not run (dry-run)`.
|
|
34
|
+
3. **TPM Data Connector installed and active** — cannot be verified programmatically. Ask the user, spelling out the exact navigation steps rather than just naming the setting: *"Can you confirm the TPM Data Connector is Active? 1) Go to Setup → Data Cloud Setup. 2) In the left nav, under EXTERNAL INTEGRATIONS, click Other Connectors. 3) Find 'TPM Data Connector' in the list and check its status."* Wait for an explicit yes. If missing or inactive, **stop** — tell the user we cannot proceed without the TPM Data Connector being active, and explain the two different paths depending on org type:
|
|
35
|
+
- **Production orgs** — it's typically auto-installed within ~24h of Processing Service pairing, provided the org has a Data Cloud license. If both prerequisites are met, ask the user to wait and retry; if either isn't met (no pairing yet, or no Data Cloud license), that's the real blocker to resolve first.
|
|
36
|
+
- **Sandbox orgs** — auto-install commonly does not happen; it typically needs to be activated manually under EXTERNAL INTEGRATIONS → Other Connectors. Tell the user this is expected in sandboxes and ask them to activate it there, then confirm before continuing.
|
|
37
|
+
Re-run this check once the user confirms it's Active.
|
|
38
|
+
4. **Salesforce CRM Home org connection** — cannot be verified programmatically. Ask the user, spelling out the exact navigation steps: *"Can you confirm the Salesforce CRM Home org connection is Active? 1) Go to Setup → Data Cloud Setup. 2) In the left nav, under SALESFORCE INTEGRATIONS, click Salesforce CRM. 3) Check that the Home org connection status is Active."* Wait for an explicit yes. If missing/inactive, **stop** — tell the user we cannot proceed without this connection being active (Phase 2's field-permission grants require the `Data Cloud Salesforce Connector` permission set, which won't exist until this connection is configured) — ask them to activate/configure it first, then re-run this check.
|
|
39
|
+
5. **Permission set(s) for the TPM Admin persona + GenieAdmin permission set confirmed assigned** — when invoked as a delegated step from `consumer-goods-tpe-dashboard-configure`, it passes through both results already confirmed in its own earlier phases; reuse them, don't re-ask or re-query. When invoked standalone, confirm each directly:
|
|
40
|
+
- **Permission set(s) for the TPM Admin persona** — "TPM Admin persona" is a role, not an actual permission set name; the org-specific permission set backing that persona (name/assignment) cannot be verified programmatically. Ask the user to confirm: *"Is the permission set(s) required for the TPM Admin persona assigned to <username>?"* Wait for an explicit yes.
|
|
41
|
+
- **GenieAdmin** — resolve its Label first (never show the API `Name` to the user), then check the assignment:
|
|
42
|
+
```bash
|
|
43
|
+
sf data query --target-org <username> --query "SELECT Label FROM PermissionSet WHERE Name = 'GenieAdmin'"
|
|
44
|
+
sf data query --target-org <username> \
|
|
45
|
+
--query "SELECT PermissionSet.Name FROM PermissionSetAssignment WHERE Assignee.Username = '<username>' AND PermissionSet.Name = 'GenieAdmin'"
|
|
46
|
+
```
|
|
47
|
+
Missing ⇒ tell the user the Label from the first query (e.g. *"User <username> is missing the <Label> permission set. Assign it now?"*) and ask permission to assign before continuing:
|
|
48
|
+
```bash
|
|
49
|
+
sf data create record --sobject PermissionSetAssignment \
|
|
50
|
+
--values "AssigneeId=<userId> PermissionSetId=<psId>" --target-org <username>
|
|
51
|
+
```
|
|
52
|
+
If declined, skip and note it in the report.
|
|
53
|
+
6. **Namespace (`NS`)** — when invoked as a delegated step, `consumer-goods-tpe-dashboard-configure` passes through its already-detected `NS` (raw namespace string); reuse it, don't re-detect. When invoked standalone, detect it once yourself, before Phase 1, with the sibling script:
|
|
54
|
+
```bash
|
|
55
|
+
node ./scripts/detect-namespace.js --target-org <username>
|
|
56
|
+
```
|
|
57
|
+
Prints `{"NS","NS_SEGMENT","NS_APEX","NS_FIELD"}` as one line of JSON. Store all four and reuse everywhere below.
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
## Phase 1 — Preflight
|
|
61
|
+
|
|
62
|
+
2. **Baseline for later verification** — capture the latest `DataKitDeploymentLog` Id now, before any deploy work, to avoid a `CreatedDate` clock-skew false-negative in Phase 5:
|
|
63
|
+
```bash
|
|
64
|
+
sf data query --target-org <username> \
|
|
65
|
+
--query "SELECT Id FROM DataKitDeploymentLog ORDER BY CreatedDate DESC LIMIT 1" --json
|
|
66
|
+
```
|
|
67
|
+
Store as `LAST_ID_BEFORE_DEPLOY` (empty if zero rows).
|
|
68
|
+
3. **Download & unzip `CGCloudAddons`** — this skill fetches it itself rather than asking the user to do it manually.
|
|
69
|
+
```bash
|
|
70
|
+
node ./scripts/download-static-resource.js \
|
|
71
|
+
--target-org <username> --name CGCloudAddons
|
|
72
|
+
```
|
|
73
|
+
Capture `STATIC_RESOURCE_PATH=<dir>` from stdout as `<addons_root>` — every path below is relative to it (e.g. `<addons_root>/TPM/Accruals/TPM Accruals Data Kit`). Zero `StaticResource` rows ⇒ **block**: the package version installed in this org doesn't ship `CGCloudAddons`. Any other script failure ⇒ block and relay its stderr verbatim.
|
|
74
|
+
|
|
75
|
+
## Phase 2 — Field permissions for CRM streams (derived from the downloaded package)
|
|
76
|
+
|
|
77
|
+
Two sibling scripts drive this phase — never hand-roll the parsing or the grant calls; see each script's own docstring for exactly what it does and why:
|
|
78
|
+
|
|
79
|
+
1. **Extract** the `{sobject, fields[]}` list from the downloaded package (read-only, always safe to run):
|
|
80
|
+
```bash
|
|
81
|
+
node ./scripts/extract-crm-field-permissions.js \
|
|
82
|
+
--dir "<addons_root>/TPM/Accruals/TPM Accruals Data Kit/force-app/main/default/dataSourceObjects" \
|
|
83
|
+
> /tmp/crm-field-permissions.json
|
|
84
|
+
```
|
|
85
|
+
Zero entries in the output ⇒ block and report — shouldn't happen if Phase 1 succeeded.
|
|
86
|
+
2. **Get the diff to update, silently** (the script's own `--dry-run` — read-only: permission set, `sobject/describe`, existing `FieldPermissions`/`ObjectPermissions` rows) — don't show the user anything yet, this is just to find out whether there's anything to do:
|
|
87
|
+
```bash
|
|
88
|
+
node ./scripts/update-field-permissions.js \
|
|
89
|
+
--target-org <username> --input /tmp/crm-field-permissions.json --dry-run
|
|
90
|
+
```
|
|
91
|
+
`{"error":"permission_set_not_found",...}` ⇒ stop this phase, mark it blocked, and tell the user: *"Could not find the 'Data Cloud Salesforce Connector' permission set — a likely explanation is that Salesforce CRM hasn't been configured yet (Setup → Data Cloud → Salesforce CRM). Please configure that first, then re-run this phase."*
|
|
92
|
+
3. **Only if `wouldGrant` or `objectWouldGrant` is non-empty**, show the user exactly that diff (not the full extracted list) and get an explicit go-ahead before writing anything. If both are empty, everything's already granted — skip straight to step 5, no confirmation needed.
|
|
93
|
+
4. **On confirmation, run it for real** — add-only at both field and object level (see script docstring for exact semantics):
|
|
94
|
+
```bash
|
|
95
|
+
node ./scripts/update-field-permissions.js \
|
|
96
|
+
--target-org <username> --input /tmp/crm-field-permissions.json
|
|
97
|
+
```
|
|
98
|
+
Exit code `3` (or a non-empty `failed[]`/`objectFailed[]`) ⇒ don't guess a workaround — prompt the user to do it manually via Setup → Permission Sets → Data Cloud Salesforce Connector → Object Settings, using exactly the `failed[]`/`objectFailed[]` list, and note each in the report.
|
|
99
|
+
5. Record pass/blocked/pending, and the touched-field/object outcome (`granted`/`alreadyGranted`/`notFlsEligible`/`failed`, `objectGranted`/`objectAlreadyGranted`/`objectFailed`), for the final report. Dry-run mode (never proceed to step 4 when in dry run mode as it updates permissions, step1 and step2 can be run as they are read only)
|
|
100
|
+
6. `objectGranted`/`objectAlreadyGranted` entries with `permissionsViewAllRecords: false` are expected for a small, fixed set of sobjects (currently just Product2) whose license blocks that flag — this is a known platform restriction the script applies silently; don't flag it to the user or treat it as something needing manual Setup action.
|
|
101
|
+
|
|
102
|
+
## Phase 3 — Prepare and deploy data kit metadata
|
|
103
|
+
|
|
104
|
+
1. In `<addons_root>/TPM/Accruals/TPM Accruals Data Kit`, find-and-replace every instance of the placeholder `__SF_ORG_ID__` with the org's 15-digit Id. Get the org Id via the following sibling script
|
|
105
|
+
```bash
|
|
106
|
+
node ./scripts/sf-rest.js org-status --target-org <username>
|
|
107
|
+
```
|
|
108
|
+
Take the `orgId` field from the printed JSON and truncate it to 15 chars (this call is cheap and idempotent — always re-run it here rather than relying on the Inputs step 1 result still being in context).
|
|
109
|
+
2. Deploy from that folder:
|
|
110
|
+
```bash
|
|
111
|
+
sf project deploy start --target-org <username>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Phase 4 — Deploy DMO mappings and run the deployment script
|
|
115
|
+
|
|
116
|
+
1. **DMO mappings** — maps the data kit's DLOs to Data 360 DMOs. From `<addons_root>/TPM/Accruals/DmoMappings`:
|
|
117
|
+
```bash
|
|
118
|
+
sf project deploy start --target-org <username>
|
|
119
|
+
```
|
|
120
|
+
2. **Deployment script** — from `<addons_root>/TPM/Accruals/Accruals Data Kit Deployment Scripts`:
|
|
121
|
+
```bash
|
|
122
|
+
SF_ORG_ALIAS=<username> node deploy-tpm-tpe.js
|
|
123
|
+
```
|
|
124
|
+
This script POSTs to `/services/data/v67.0/actions/custom/flow/sfdatakit__DeployDataKitComponents` and returns an async Flow interview guid immediately; that response alone doesn't mean success — always confirm via the `DataKitDeploymentLog` check in Phase 5. This script runs from inside the downloaded/unzipped static resource.
|
|
125
|
+
|
|
126
|
+
## Phase 5 — Verify
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
sf data query --target-org <username> \
|
|
130
|
+
--query "SELECT Id, DeploymentStatus, ComponentName, DeploymentError, CreatedDate FROM DataKitDeploymentLog WHERE Id > '<LAST_ID_BEFORE_DEPLOY>' ORDER BY CreatedDate DESC"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Every row for this run must have `DeploymentStatus = 'Successful'` and a blank `DeploymentError`. **Never claim success while any row is `Failure` or has a non-empty error.**
|
|
134
|
+
|
|
135
|
+
Alternative UI check: Data Cloud Setup → Data Kits → TPM Accruals → Local Deployment History → status `Successful`.
|
|
136
|
+
|
|
137
|
+
## Known gotchas
|
|
138
|
+
|
|
139
|
+
- `FlowDefinition` is **not** SOQL-queryable — don't probe it for readiness.
|
|
140
|
+
- An `sfdatakit__DeployDataKitComponents` ApexClass probe can return 0 rows even when the framework is live — prefer probing `DataKitDeploymentLog` directly (Phase 5), never the Apex class.
|
|
141
|
+
|
|
142
|
+
## Rules
|
|
143
|
+
|
|
144
|
+
- Never claim success while any `DataKitDeploymentLog` row for this run is `Failure` or has a non-empty `DeploymentError`.
|
|
145
|
+
- Only ever read the `CGCloudAddons` `StaticResource` (via `download-static-resource.js`) — never deploy or modify it; the org's own `sf`-authenticated access token drives the download, never a hardcoded credential.
|
|
146
|
+
- Never probe `FlowDefinition` or the `sfdatakit__DeployDataKitComponents` Apex class for readiness — probe `DataKitDeploymentLog` instead.
|
|
147
|
+
- Never consult the org-shipped `README.md` (or any other in-package documentation bundled inside `CGCloudAddons`) for deployment guidance — follow this skill's phases exactly as written. If a script name or path documented here doesn't match what's actually in the downloaded package, that's a bug in this skill to fix (update the skill itself), not a cue to switch to package docs at runtime.
|
|
148
|
+
- If a script or folder is missing from the downloaded folder, do not guess or change the scripts. e.g.: never change the script `deploy-tpm-tpe.js`.
|
|
149
|
+
- Never run `update-field-permissions.js` without `--dry-run` first. If its `wouldGrant`/`objectWouldGrant` come back non-empty, never run for real without showing the user that exact diff and getting an explicit go-ahead — same as any other write in this skill. If both are empty, no confirmation is needed; proceed straight to reporting.
|
|
150
|
+
- `update-field-permissions.js` only ever adds — a new `FieldPermissions`/`ObjectPermissions` row where none existed, or flipping an existing row's off flag (`PermissionsRead`, or `PermissionsViewAllRecords` at the object level) to `true`. Never call it in a way that deletes a row, weakens an existing grant, or touches `PermissionsCreate`/`Edit`/`Delete`/`ModifyAllRecords`.
|
|
151
|
+
- Never guess a workaround when the `sfdc_a360_sfcrm_data_extract` ("Data Cloud Salesforce Connector") permission set is missing or a field/object ends up in `update-field-permissions.js`'s `failed[]`/`objectFailed[]` — stop and prompt the user to grant field-level or object-level security manually via Setup (see Phase 2).
|
|
152
|
+
|
|
153
|
+
## Report
|
|
154
|
+
|
|
155
|
+
Produce a final report (pass/blocked/pending) covering: CRM stream field permissions (per-object field list from `extract-crm-field-permissions.js`, and each field's `granted`/`alreadyGranted`/`notFlsEligible`/`failed` outcome from `update-field-permissions.js` — flag any `notFlsEligible` entry with `onObject: false` separately, since that's a package/org data mismatch, not a normal skip), CRM stream object-level permissions (each sobject's `objectGranted`/`objectAlreadyGranted`/`objectFailed` outcome from the same script — a `permissionsViewAllRecords: false` on Product2 specifically is expected and not worth calling out), preflight (connector status), metadata deploy, DMO mapping deploy, deployment script run, and the `DataKitDeploymentLog` verification result (row count, any failures). This is exactly what `consumer-goods-tpe-dashboard-configure` Step 7 relays as its own result when this skill is invoked via delegation.
|
|
156
|
+
|
|
157
|
+
**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,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,151 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Downloads and unzips a StaticResource from an authenticated org, reusing
|
|
4
|
+
* the same contract as `downloadStaticResource` in
|
|
5
|
+
* `packages/liability-accruals/setup/steps/01-download-static-resource.js`
|
|
6
|
+
* (SOQL for the StaticResource Id, then an authenticated GET against
|
|
7
|
+
* `<instanceUrl>/services/data/v<apiVersion>/sobjects/StaticResource/<id>/Body`,
|
|
8
|
+
* then extract the zip) — reimplemented here with Node's built-in fetch
|
|
9
|
+
* instead of `curl`, so this skill has no dependency on that script. Zip
|
|
10
|
+
* extraction shells out to a platform-native tool rather than an npm
|
|
11
|
+
* package (`adm-zip`, etc.) so this skill stays dependency-free and portable
|
|
12
|
+
* across machines that clone/copy it standalone, per this repo's
|
|
13
|
+
* self-contained-skills rule:
|
|
14
|
+
* - Windows: `tar -xf` — the `tar.exe` bundled with Windows 10 (1803+) and
|
|
15
|
+
* 11 is bsdtar, which reads zip archives transparently.
|
|
16
|
+
* - macOS/Linux: `unzip` — preinstalled on macOS; on Linux it's a common
|
|
17
|
+
* but not universal package (missing ⇒ clear error telling the user to
|
|
18
|
+
* install it). GNU `tar` on Linux cannot read zip, so `tar` is not a
|
|
19
|
+
* safe cross-platform substitute there.
|
|
20
|
+
*
|
|
21
|
+
* One deliberate deviation from that reference: `getOrgDetails()` there
|
|
22
|
+
* reads `sf org display --json`'s `result.accessToken` directly, which no
|
|
23
|
+
* longer returns a real token on `sf` CLI >= 2.136.8 (that field becomes a
|
|
24
|
+
* redacted hint string pointing at `sf org auth show-access-token` —
|
|
25
|
+
* confirmed live against this repo's own dev org). `getAccessToken()` in
|
|
26
|
+
* ``./sf-rest` is version-gated to handle both cases; the token is never
|
|
27
|
+
* printed.
|
|
28
|
+
*
|
|
29
|
+
* Usage:
|
|
30
|
+
* node download-static-resource.js \
|
|
31
|
+
* --target-org <username> \
|
|
32
|
+
* --name CGCloudAddons \
|
|
33
|
+
* [--output-dir <dir>] # defaults to a fresh dir under the OS temp dir
|
|
34
|
+
*
|
|
35
|
+
* Writes <output-dir>/<name>.zip and unzips it to <output-dir>/<name>/.
|
|
36
|
+
* Prints STATIC_RESOURCE_PATH=<absolute unzipped dir> on success.
|
|
37
|
+
*
|
|
38
|
+
* Exit codes: 0 = success, 1 = resource not found, org not connected, or
|
|
39
|
+
* download/unzip failure.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
const fs = require('fs');
|
|
43
|
+
const os = require('os');
|
|
44
|
+
const path = require('path');
|
|
45
|
+
const { execFileSync } = require('child_process');
|
|
46
|
+
const { sfJson, getAccessToken } = require('./sf-rest');
|
|
47
|
+
|
|
48
|
+
function parseArgs(argv) {
|
|
49
|
+
const args = {};
|
|
50
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
51
|
+
const token = argv[i];
|
|
52
|
+
if (!token.startsWith('--')) continue;
|
|
53
|
+
args[token.slice(2)] = argv[i + 1];
|
|
54
|
+
i += 1;
|
|
55
|
+
}
|
|
56
|
+
return args;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function requireArg(args, name) {
|
|
60
|
+
if (!args[name]) {
|
|
61
|
+
console.error(`Missing required --${name}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
return args[name];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function unzipTo(zipPath, destDir) {
|
|
68
|
+
const [cmd, cmdArgs] = process.platform === 'win32'
|
|
69
|
+
? ['tar', ['-xf', zipPath, '-C', destDir]]
|
|
70
|
+
: ['unzip', ['-o', '-q', zipPath, '-d', destDir]];
|
|
71
|
+
const attemptedCommand = `${cmd} ${cmdArgs.join(' ')}`;
|
|
72
|
+
try {
|
|
73
|
+
execFileSync(cmd, cmdArgs, { stdio: 'pipe' });
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error.code === 'ENOENT') {
|
|
76
|
+
console.error(
|
|
77
|
+
`download-static-resource: command failed — "${attemptedCommand}" — "${cmd}" is not installed or not on PATH — ${
|
|
78
|
+
process.platform === 'win32'
|
|
79
|
+
? 'expected the tar.exe bundled with Windows 10 (1803+) / 11'
|
|
80
|
+
: 'install it (e.g. `apt install unzip`) and retry'
|
|
81
|
+
}. If a different extraction command is available on this machine, run it manually against "${zipPath}" -> "${destDir}" and re-invoke this script with the same --output-dir to skip re-downloading.`
|
|
82
|
+
);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
console.error(
|
|
86
|
+
`download-static-resource: command failed — "${attemptedCommand}" — ${error.message}`
|
|
87
|
+
);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function main() {
|
|
93
|
+
const args = parseArgs(process.argv.slice(2));
|
|
94
|
+
const targetOrg = requireArg(args, 'target-org');
|
|
95
|
+
const name = requireArg(args, 'name');
|
|
96
|
+
const outputDir =
|
|
97
|
+
args['output-dir'] ||
|
|
98
|
+
fs.mkdtempSync(path.join(os.tmpdir(), 'cgcloud-static-resource-'));
|
|
99
|
+
|
|
100
|
+
const queryResult = sfJson([
|
|
101
|
+
'data',
|
|
102
|
+
'query',
|
|
103
|
+
'--target-org',
|
|
104
|
+
targetOrg,
|
|
105
|
+
'--query',
|
|
106
|
+
`SELECT Id, Name FROM StaticResource WHERE Name='${name}'`,
|
|
107
|
+
'--json',
|
|
108
|
+
]);
|
|
109
|
+
if (!queryResult.records || queryResult.records.length < 1) {
|
|
110
|
+
console.error(
|
|
111
|
+
`download-static-resource: no StaticResource named "${name}" found in ${targetOrg} — confirm the package version installed in this org includes it.`
|
|
112
|
+
);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
const staticResourceId = queryResult.records[0].Id;
|
|
116
|
+
|
|
117
|
+
const orgDetails = sfJson(['org', 'display', '--target-org', targetOrg, '--json']);
|
|
118
|
+
const instanceUrl = orgDetails.instanceUrl;
|
|
119
|
+
const apiVersion = orgDetails.apiVersion || '60.0';
|
|
120
|
+
const accessToken = getAccessToken(targetOrg);
|
|
121
|
+
|
|
122
|
+
const bodyUrl = `${instanceUrl}/services/data/v${apiVersion}/sobjects/StaticResource/${staticResourceId}/Body`;
|
|
123
|
+
const response = await fetch(bodyUrl, {
|
|
124
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
125
|
+
});
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
console.error(
|
|
128
|
+
`download-static-resource: download failed with HTTP ${response.status}`
|
|
129
|
+
);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
133
|
+
|
|
134
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
135
|
+
const zipPath = path.join(outputDir, `${name}.zip`);
|
|
136
|
+
fs.writeFileSync(zipPath, buffer);
|
|
137
|
+
|
|
138
|
+
const unzipDir = path.join(outputDir, name);
|
|
139
|
+
if (fs.existsSync(unzipDir)) {
|
|
140
|
+
fs.rmSync(unzipDir, { recursive: true, force: true });
|
|
141
|
+
}
|
|
142
|
+
fs.mkdirSync(unzipDir, { recursive: true });
|
|
143
|
+
unzipTo(zipPath, unzipDir);
|
|
144
|
+
|
|
145
|
+
console.log(`STATIC_RESOURCE_PATH=${path.resolve(unzipDir)}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
main().catch((err) => {
|
|
149
|
+
console.error(err);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
});
|