@salesforce/afv-skills 1.40.0 → 1.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/package.json +1 -1
  2. package/skills/automation-flow-generate/SKILL.md +32 -43
  3. package/skills/experience-portal-create/SKILL.md +497 -0
  4. package/skills/experience-portal-create/assets/report-template.md +30 -0
  5. package/skills/experience-portal-create/references/mcp-invocation.md +288 -0
  6. package/skills/experience-portal-create/references/post-creation-activate-publish.md +165 -0
  7. package/skills/experience-portal-create/references/templates.md +253 -0
  8. package/skills/experience-ui-bundle-features-generate/SKILL.md +5 -1
  9. package/skills/experience-ui-bundle-frontend-generate/SKILL.md +2 -0
  10. package/skills/experience-ui-bundle-frontend-generate/references/page.md +1 -0
  11. package/skills/platform-datamask-run/SKILL.md +345 -0
  12. package/skills/platform-datamask-run/references/api-surface.md +130 -0
  13. package/skills/platform-datamask-run/references/policy-authoring.md +185 -0
  14. package/skills/platform-datamask-run/references/run-and-abort.md +116 -0
  15. package/skills/platform-datamask-run/scripts/poll-job.sh +115 -0
  16. package/skills/platform-dataspace-access-configure/SKILL.md +51 -3
  17. package/skills/platform-dataspace-access-configure/scripts/inspect-dataspace-scopes.sh +56 -0
  18. package/skills/platform-lightning-type-widget-coordinate/references/build-plan-format.md +1 -0
  19. package/skills/platform-sandbox-configure/SKILL.md +17 -2
  20. package/skills/platform-trial-org-create/SKILL.md +175 -0
  21. package/skills/platform-trial-org-create/examples/create_request.json +9 -0
  22. package/skills/platform-trial-org-create/examples/error_response.json +41 -0
  23. package/skills/platform-trial-org-create/examples/success_response.json +27 -0
  24. package/skills/platform-trial-org-create/references/error_codes.md +42 -0
  25. package/skills/platform-trial-org-create/references/signup_request_fields.md +69 -0
  26. package/skills/platform-trial-org-create/scripts/create_signup_request.sh +175 -0
  27. package/skills/platform-trial-org-create/scripts/get_signup_request.sh +155 -0
  28. package/skills/platform-widget-generate/SKILL.md +47 -6
  29. package/skills/platform-widget-generate/examples/conditional.json +3 -3
  30. package/skills/platform-widget-generate/examples/list-with-foreach.json +2 -2
  31. package/skills/platform-widget-generate/examples/single-object.json +2 -2
  32. package/skills/platform-widget-generate/references/widget-bundle-layout.md +1 -1
  33. package/skills/service-agentforce-channel-configure/SKILL.md +271 -0
  34. package/skills/service-agentforce-channel-configure/references/agent-wiring.md +97 -0
  35. package/skills/service-agentforce-channel-configure/references/channel-branch-email.md +145 -0
  36. package/skills/service-agentforce-channel-configure/references/channel-branch-voice.md +69 -0
  37. package/skills/service-agentforce-channel-configure/references/channel-types.md +61 -0
  38. package/skills/service-agentforce-channel-configure/references/live-traffic-gate.md +86 -0
  39. package/skills/service-agentforce-channel-configure/references/queue-resolution.md +135 -0
  40. package/skills/service-agentforce-channel-configure/references/routing-flow.md +384 -0
  41. package/skills/service-catalog-template-deploy/SKILL.md +310 -0
  42. package/skills/service-catalog-template-deploy/references/cli-invocation.md +258 -0
  43. package/skills/service-catalog-template-deploy/scripts/activate-verify.mjs +164 -0
  44. package/skills/service-catalog-template-deploy/scripts/build-deploy-payload.mjs +94 -0
  45. package/skills/service-catalog-template-deploy/scripts/resolve-template.mjs +331 -0
  46. package/skills/service-catalog-template-search/SKILL.md +212 -0
  47. package/skills/service-catalog-template-search/references/cli-invocation.md +128 -0
  48. package/skills/service-catalog-template-search/scripts/classify-catalog.mjs +205 -0
  49. package/skills/service-concierge-portal-generate/SKILL.md +126 -0
  50. package/skills/service-concierge-portal-generate/references/portal-deploy-runbook.md +1428 -0
  51. package/skills/service-digital-engagement-channel-configure/SKILL.md +46 -6
  52. package/skills/service-digital-engagement-channel-configure/assets/messaging_channel_template.xml +2 -1
  53. package/skills/service-digital-engagement-channel-configure/examples/asa_agent_channel.xml +4 -1
  54. package/skills/service-helpagent-coordinate/README.md +8 -2
  55. package/skills/service-helpagent-coordinate/SKILL.md +126 -130
  56. package/skills/service-helpagent-coordinate/assets/help-agent-spec.md +70 -53
  57. package/skills/service-helpagent-coordinate/references/agent-script.md +571 -457
  58. package/skills/service-helpagent-coordinate/references/channel-voice.md +38 -9
  59. package/skills/service-helpagent-coordinate/references/channel-web-chat.md +173 -49
  60. package/skills/service-helpagent-coordinate/references/output-report-format.md +126 -0
  61. package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +153 -0
  62. package/skills/service-itsm-agentic-setup-agentforce-coordinate/examples/output-templates.md +79 -0
  63. package/skills/service-itsm-agentic-setup-agentforce-coordinate/scripts/verify-child-verdict.mjs +35 -0
  64. package/skills/service-itsm-agentic-setup-agentforce-studio-configure/SKILL.md +271 -0
  65. package/skills/service-itsm-agentic-setup-agentforce-studio-configure/references/cli-invocation.md +265 -0
  66. package/skills/service-itsm-agentic-setup-agentforce-studio-configure/scripts/classify-enable-plan.mjs +220 -0
  67. package/skills/service-itsm-agentic-setup-agentforce-studio-configure/scripts/classify-final-report.mjs +102 -0
  68. package/skills/service-itsm-agentic-setup-agentforce-studio-configure/scripts/record-enable-result.mjs +73 -0
  69. package/skills/service-itsm-agentic-setup-agentforce-studio-validate/SKILL.md +206 -0
  70. package/skills/service-itsm-agentic-setup-agentforce-studio-validate/references/cli-invocation.md +194 -0
  71. package/skills/service-itsm-agentic-setup-agentforce-studio-validate/scripts/classify-readiness.mjs +223 -0
  72. package/skills/service-itsm-agentic-setup-cmdb-configure/SKILL.md +45 -7
  73. package/skills/service-itsm-agentic-setup-cmdb-configure/references/mcp-invocation.md +45 -5
  74. package/skills/service-itsm-agentic-setup-configure/SKILL.md +116 -0
  75. package/skills/service-itsm-agentic-setup-configure/examples/output-templates.md +64 -0
  76. package/skills/service-itsm-agentic-setup-employee-agent-configure/SKILL.md +158 -0
  77. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/cli-invocation.md +361 -0
  78. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/error-taxonomy.md +44 -0
  79. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/reactivation.md +66 -0
  80. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/report-format.md +66 -0
  81. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/specialized-templates.md +148 -0
  82. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/workflow-detail.md +169 -0
  83. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/build-create-body.mjs +116 -0
  84. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/classify-agent-existence.mjs +185 -0
  85. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/classify-preflight.mjs +168 -0
  86. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/create-scratch-dir.mjs +58 -0
  87. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/render-report.mjs +197 -0
  88. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/SKILL.md +186 -0
  89. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/action-availability.md +51 -0
  90. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/cli-invocation.md +345 -0
  91. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/error-taxonomy.md +44 -0
  92. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/reactivation.md +63 -0
  93. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/report-format.md +44 -0
  94. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/workflow-detail.md +149 -0
  95. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/build-create-body.mjs +110 -0
  96. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-action-availability.mjs +201 -0
  97. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-activate-result.mjs +135 -0
  98. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-agent-existence.mjs +194 -0
  99. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-preflight.mjs +158 -0
  100. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/create-scratch-dir.mjs +58 -0
  101. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/render-report.mjs +191 -0
  102. package/skills/service-itsm-agentic-setup-incident-management/SKILL.md +133 -0
  103. package/skills/service-itsm-agentic-setup-incident-management/examples/output-templates.md +71 -0
  104. package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +308 -0
  105. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone.json +23 -0
  106. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/milestone-patterns.md +193 -0
  107. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/output-templates.md +57 -0
  108. package/skills/service-itsm-agentic-setup-incident-sla-configure/references/mcp-invocation.md +394 -0
  109. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/SKILL.md +266 -0
  110. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/references/cli-invocation.md +106 -0
  111. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/references/helper-contracts.md +142 -0
  112. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/references/permset-topology.md +82 -0
  113. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/scripts/classify-action-surface.mjs +137 -0
  114. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/scripts/classify-assignment-state.mjs +99 -0
  115. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/scripts/classify-permset-availability.mjs +120 -0
  116. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/scripts/resolve-target-user.mjs +86 -0
  117. package/skills/service-itsm-agentic-setup-uel-user-create/SKILL.md +284 -0
  118. package/skills/service-itsm-agentic-setup-uel-user-create/references/mcp-invocation.md +302 -0
  119. package/skills/service-itsm-channels-coordinate/SKILL.md +472 -0
  120. package/skills/service-itsm-incident-mgmt-configure/SKILL.md +212 -0
  121. package/skills/service-itsm-incident-mgmt-configure/references/mcp-invocation.md +225 -0
  122. package/skills/service-itsm-incident-priority-configure/SKILL.md +53 -12
  123. package/skills/service-itsm-swarming-configure/SKILL.md +212 -0
  124. package/skills/service-itsm-teams-configure/SKILL.md +395 -0
  125. package/skills/service-itsm-teams-configure/references/azure-credential-population.md +213 -0
  126. package/skills/service-itsm-teams-configure/references/gotchas.md +23 -0
  127. package/skills/service-itsm-teams-coordinate/SKILL.md +175 -0
  128. package/skills/service-itsm-teams-coordinate/examples/output-templates.md +85 -0
  129. package/skills/service-itsm-teams-debug/SKILL.md +144 -0
  130. package/skills/service-itsm-teams-debug/references/configuration-checklists.md +277 -0
  131. package/skills/service-itsm-teams-debug/references/report-generation.md +95 -0
  132. package/skills/service-itsm-teams-employee-agent-configure/SKILL.md +139 -0
  133. package/skills/service-itsm-teams-employee-agent-configure/assets/Teams_AgentForce.EmbeddedServiceConfig-meta.xml +43 -0
  134. package/skills/service-itsm-teams-employee-agent-configure/references/teams-embedded-employee-agent.md +480 -0
  135. package/skills/service-itsm-teams-itdesk-configure/SKILL.md +232 -0
  136. package/skills/service-itsm-teams-itservice-configure/SKILL.md +391 -0
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+ // Injection-safe activate-and-verify helper (authoring standards: Agent Safety + A9).
3
+ //
4
+ // SECURITY (the BLOCKER this script exists to fix): the Service Process name is
5
+ // USER-SUPPLIED. It must never be interpolated into a double-quoted Bash command
6
+ // or a single-quoted SOQL literal — a name containing shell metacharacters or a
7
+ // stray quote could execute during command construction or alter the query and
8
+ // touch an unintended Product2. This script:
9
+ // 1. accepts the name as an argv ARGUMENT (no shell interpolation ever), and
10
+ // 2. invokes `sf` via execFileSync with an ARGUMENT ARRAY (shell:false), and
11
+ // 3. escapes the name for the SOQL string literal before embedding it.
12
+ //
13
+ // A9: the resolve-Id → conditional-activate → re-read sequence is a fixed
14
+ // multi-command operation with query parsing and conditional DML; encoding it
15
+ // here keeps it deterministic instead of relying on model interpretation.
16
+ //
17
+ // Usage:
18
+ // node activate-verify.mjs <serviceProcessName> [--target-org <alias>] [--no-activate]
19
+ //
20
+ // --no-activate : verify only (use when the user asked to leave it inactive).
21
+ //
22
+ // Prints a JSON result: { found, id, isActive, activated, verified }.
23
+ // Exit 0 when the operation completed (found or honestly not-found); exit 2 on
24
+ // bad args; exit 1 only when an sf call errors unexpectedly.
25
+
26
+ import { execFileSync } from 'node:child_process';
27
+
28
+ const argv = process.argv.slice(2);
29
+ const noActivate = argv.includes('--no-activate');
30
+ const orgIdx = argv.indexOf('--target-org');
31
+ const targetOrg = orgIdx !== -1 ? argv[orgIdx + 1] : null;
32
+ // Positional name = first non-flag token that is NOT the --target-org value.
33
+ // Excluding the alias slot stops a flags-first call
34
+ // (`--target-org myOrg "My Template"`) from mistaking the alias for the name.
35
+ // Guard on orgIdx !== -1: when --target-org is absent, orgIdx is -1 and a bare
36
+ // `orgIdx + 1` would be 0 and wrongly exclude a name sitting at argv[0]. Setting
37
+ // the exclude slot to -1 in that case means it matches no real index.
38
+ const aliasIdx = orgIdx === -1 ? -1 : orgIdx + 1;
39
+ const name = argv.find((a, i) => !a.startsWith('--') && i !== aliasIdx);
40
+
41
+ if (!name) {
42
+ process.stderr.write('usage: node activate-verify.mjs <serviceProcessName> [--target-org <alias>] [--no-activate]\n');
43
+ process.exit(2);
44
+ }
45
+
46
+ // Escape a value for embedding inside a single-quoted SOQL string literal.
47
+ // SOQL uses backslash escaping: backslash and single-quote must be escaped.
48
+ // (Reject any residual control chars defensively.)
49
+ function soqlLiteral(value) {
50
+ return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/[\n\r\t]/g, ' ');
51
+ }
52
+
53
+ // Run `sf` with an argument ARRAY — no shell, so nothing in `name` can be
54
+ // interpreted as a shell token. Returns parsed --json stdout.
55
+ function sf(args) {
56
+ const full = targetOrg ? [...args, '--target-org', targetOrg] : args;
57
+ try {
58
+ const out = execFileSync('sf', full, { encoding: 'utf8', maxBuffer: 20 * 1024 * 1024 });
59
+ return { ok: true, json: JSON.parse(out) };
60
+ } catch (err) {
61
+ // sf exits non-zero on query/DML errors; stdout may still hold --json.
62
+ const stdout = err.stdout ? String(err.stdout) : '';
63
+ try {
64
+ return { ok: false, json: JSON.parse(stdout) };
65
+ } catch {
66
+ return { ok: false, json: null, raw: stdout || String(err.message) };
67
+ }
68
+ }
69
+ }
70
+
71
+ const literal = soqlLiteral(name);
72
+ const findQuery =
73
+ `SELECT Id, Name, IsActive FROM Product2 ` +
74
+ `WHERE Name='${literal}' AND UsedFor='ServiceProcess' ` +
75
+ `ORDER BY CreatedDate DESC LIMIT 1`;
76
+
77
+ // ---- 1. resolve the deployed Product2 by name (no shell; name is a query arg)
78
+ const found = sf(['data', 'query', '--query', findQuery, '--json']);
79
+
80
+ // A failed CLI query (auth/transport/malformed) must NOT be reported as a
81
+ // verified absence — `found:false` would be indistinguishable from a real
82
+ // not-found and could let a caller conclude the deploy silently vanished.
83
+ // Surface the error and exit non-zero so the caller treats it as a check failure.
84
+ if (!found.ok) {
85
+ const detail =
86
+ found.json?.message ||
87
+ found.json?.[0]?.message ||
88
+ found.raw ||
89
+ 'sf data query failed';
90
+ process.stderr.write(`activate-verify: initial Product2 lookup failed — ${detail}\n`);
91
+ process.stdout.write(
92
+ JSON.stringify({
93
+ error: true,
94
+ found: null,
95
+ id: null,
96
+ isActive: null,
97
+ activated: false,
98
+ verified: false,
99
+ detail: String(detail),
100
+ }) + '\n',
101
+ );
102
+ process.exit(1);
103
+ }
104
+
105
+ const rec = found.json?.result?.records?.[0];
106
+
107
+ if (!rec) {
108
+ process.stdout.write(
109
+ JSON.stringify({ found: false, id: null, isActive: null, activated: false, verified: false }) + '\n',
110
+ );
111
+ process.exit(0);
112
+ }
113
+
114
+ let activated = false;
115
+
116
+ // ---- 2. activate (conditional) — only if not already active and allowed ----
117
+ if (!noActivate && rec.IsActive !== true) {
118
+ const upd = sf(['data', 'update', 'record', '--sobject', 'Product2', '--record-id', rec.Id, '--values', 'IsActive=true', '--json']);
119
+ activated = upd.ok;
120
+ }
121
+
122
+ // ---- 3. verify by re-reading -----------------------------------------------
123
+ const verifyRes = sf(['data', 'query', '--query', findQuery, '--json']);
124
+
125
+ // Symmetric to the initial-lookup guard above: a failed verification re-read
126
+ // (auth/transport/malformed) must NOT collapse to verified:false — that is
127
+ // indistinguishable from a genuine "record absent after activate" and could
128
+ // mask a deploy that actually landed. Surface the check error and exit non-zero.
129
+ // `activated` still reflects what the DML step reported.
130
+ if (!verifyRes.ok) {
131
+ const detail =
132
+ verifyRes.json?.message ||
133
+ verifyRes.json?.[0]?.message ||
134
+ verifyRes.raw ||
135
+ 'sf data query failed';
136
+ process.stderr.write(`activate-verify: verification re-read failed — ${detail}\n`);
137
+ process.stdout.write(
138
+ JSON.stringify({
139
+ error: true,
140
+ found: true,
141
+ id: rec.Id,
142
+ isActive: null,
143
+ activated,
144
+ verified: false,
145
+ detail: String(detail),
146
+ }) + '\n',
147
+ );
148
+ process.exit(1);
149
+ }
150
+
151
+ const verifyRec = verifyRes.json?.result?.records?.[0];
152
+ const isActive = verifyRec?.IsActive === true;
153
+ const verified = Boolean(verifyRec) && (noActivate || isActive);
154
+
155
+ process.stdout.write(
156
+ JSON.stringify({
157
+ found: true,
158
+ id: verifyRec?.Id ?? rec.Id,
159
+ isActive,
160
+ activated,
161
+ verified,
162
+ }) + '\n',
163
+ );
164
+ process.exit(0);
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ // Deterministic deploy-body builder (authoring standard A9).
3
+ //
4
+ // Transforming templateDependencyMetadata into the deploy POST body's
5
+ // flowTemplates[] is a FIXED transformation — iterate dependencies, pick the
6
+ // primary key with a defined fallback, and pass enum values through VERBATIM
7
+ // (SCREAMING_SNAKE_CASE). Doing it in prose invites the model to normalize
8
+ // casing or hardcode a literal, which the live API rejects. This script
9
+ // guarantees a consistent body and never mutates enum casing.
10
+ //
11
+ // Usage:
12
+ // node build-deploy-payload.mjs <resolved.json> [optional-fields.json]
13
+ //
14
+ // <resolved.json> is a FILE PATH to either:
15
+ // - the object emitted by resolve-template.mjs (its .resolved.templateDependencyMetadata is used), OR
16
+ // - a bare templateDependencyMetadata array, OR
17
+ // - a single template object carrying templateDependencyMetadata.
18
+ // [optional-fields.json] is a FILE PATH to a JSON object of user-supplied
19
+ // optional keys to merge in (description / isActive / deploymentMode / catalog
20
+ // / category / serviceProcessName). Only keys the user actually supplied should
21
+ // be present; absent keys are omitted from the body entirely.
22
+ //
23
+ // Prints the deploy POST body to stdout. Exit 0 on success; exit 2 on bad args.
24
+
25
+ import { readFileSync } from 'node:fs';
26
+
27
+ const [resolvedPath, optionalPath] = process.argv.slice(2);
28
+ if (!resolvedPath) {
29
+ process.stderr.write('usage: node build-deploy-payload.mjs <resolved.json> [optional-fields.json]\n');
30
+ process.exit(2);
31
+ }
32
+
33
+ function readJson(path) {
34
+ try {
35
+ return JSON.parse(readFileSync(path, 'utf8'));
36
+ } catch (err) {
37
+ process.stderr.write(`build-deploy-payload: cannot read/parse ${path}: ${err.message}\n`);
38
+ process.exit(2);
39
+ }
40
+ }
41
+
42
+ const input = readJson(resolvedPath);
43
+
44
+ // Accept any of the three shapes described above.
45
+ let deps;
46
+ if (Array.isArray(input)) {
47
+ deps = input;
48
+ } else if (Array.isArray(input?.resolved?.templateDependencyMetadata)) {
49
+ deps = input.resolved.templateDependencyMetadata;
50
+ } else if (Array.isArray(input?.templateDependencyMetadata)) {
51
+ deps = input.templateDependencyMetadata;
52
+ } else {
53
+ // No recognized deploy-ready shape: not a bare templateDependencyMetadata
54
+ // array, not a resolve-template.mjs DEPLOY output (its `.resolved` is null on a
55
+ // STOP_AMBIGUOUS / STOP_NOT_FOUND / SELF_HEAL), and not a single template
56
+ // object. Fail loud rather than emit a valid-but-empty {"flowTemplates":[]}
57
+ // that is indistinguishable from a dependency-free template — piping a stop /
58
+ // null resolution into the builder must produce an error signal, never a body.
59
+ process.stderr.write(
60
+ 'build-deploy-payload: input is not a deploy-ready template (no ' +
61
+ 'templateDependencyMetadata found; a STOP / null resolution has ' +
62
+ 'resolved:null and cannot build a deploy body). Refusing to emit an empty body.\n',
63
+ );
64
+ process.exit(2);
65
+ }
66
+
67
+ // The ONLY constructed field is templateVariables (defaults to {}); every enum
68
+ // is echoed verbatim, with a defined fallback key when the primary is absent.
69
+ const flowTemplates = deps.map((dep) => {
70
+ const el = {
71
+ templateType: dep.templateType, // verbatim: "INTAKE" | "FULFILLMENT"
72
+ templateApiName: dep.templateApiName ?? dep.dependencyApiName, // first non-empty
73
+ templateDependencyType: dep.templateDependencyType ?? dep.dependencyType, // verbatim: "FLOW"
74
+ dependencyDeploymentMedium: dep.dependencyDeploymentMedium, // verbatim: "APP_FRAMEWORK"
75
+ templateVariables:
76
+ dep.templateVariables && typeof dep.templateVariables === 'object' ? dep.templateVariables : {},
77
+ };
78
+ return el;
79
+ });
80
+
81
+ const body = { flowTemplates };
82
+
83
+ // Merge user-supplied optional fields — but only keys that are actually present
84
+ // (never send empty strings/nulls; omit the key entirely).
85
+ if (optionalPath) {
86
+ const optional = readJson(optionalPath);
87
+ for (const key of ['description', 'isActive', 'deploymentMode', 'catalog', 'category', 'serviceProcessName']) {
88
+ if (optional[key] !== undefined && optional[key] !== null && optional[key] !== '') {
89
+ body[key] = optional[key];
90
+ }
91
+ }
92
+ }
93
+
94
+ process.stdout.write(JSON.stringify(body) + '\n');
@@ -0,0 +1,331 @@
1
+ #!/usr/bin/env node
2
+ // Deterministic catalog-probe + name-resolution classifier (authoring standard A9).
3
+ //
4
+ // The access probe, HTTP-status parsing, self-heal decision, and the
5
+ // case-insensitive name→template resolution are a FIXED algorithm — parsing an
6
+ // HTTP status line, branching on 200/403/404/empty, and counting name matches
7
+ // must not be left to the model to eyeball in prose. This script encodes all of
8
+ // it; the skill's Bash steps only run the network calls its output dictates
9
+ // (the GET probe, and — on SELF_HEAL — the permset assigns), so every write
10
+ // stays visible in the trace.
11
+ //
12
+ // Usage:
13
+ // node resolve-template.mjs <get-i-output.json> [requestedName]
14
+ //
15
+ // <get-i-output.json> is a FILE PATH holding the raw stdout of:
16
+ // sf api request rest '.../service-process/get-all-templates' --method GET -i --target-org <alias>
17
+ // i.e. an HTTP status line, headers, a blank line, then the raw Connect body.
18
+ // [requestedName] is the template name the user asked to deploy (optional; when
19
+ // omitted the script only classifies access and does not resolve a match).
20
+ //
21
+ // Prints a structured outcome object to stdout. Exit code is 0 whenever the
22
+ // input was readable (a 403/404/empty catalog is a RECORDED outcome, not a
23
+ // script failure); exit 2 only on bad arguments / unreadable input.
24
+
25
+ import { readFileSync, writeSync } from 'node:fs';
26
+
27
+ const [inputPath, requestedName] = process.argv.slice(2);
28
+ if (!inputPath) {
29
+ process.stderr.write('usage: node resolve-template.mjs <get-i-output.json> [requestedName]\n');
30
+ process.exit(2);
31
+ }
32
+
33
+ let rawOutput;
34
+ try {
35
+ rawOutput = readFileSync(inputPath, 'utf8');
36
+ } catch {
37
+ process.stderr.write(`resolve-template: cannot read ${inputPath}\n`);
38
+ process.exit(2);
39
+ }
40
+
41
+ // ---- strip ANSI color codes the CLI adds to header lines --------------------
42
+ const clean = rawOutput.replace(/\x1b\[[0-9;]*m/g, '');
43
+
44
+ // ---- read the HTTP status from the first status line (may be absent if the --
45
+ // ---- caller forgot -i; then we fall back to body-shape detection) -----------
46
+ const statusMatch = clean.match(/^HTTP\/[\d.]+\s+(\d{3})/m);
47
+ const httpStatus = statusMatch ? parseInt(statusMatch[1], 10) : null;
48
+
49
+ // ---- slice the JSON body: from the first line that begins with { or [ --------
50
+ function extractBody(text) {
51
+ const idx = text.search(/^[[{]/m);
52
+ return idx === -1 ? '' : text.slice(idx);
53
+ }
54
+
55
+ // ---- control-char–safe JSON parse: the live catalog embeds raw newlines/tabs -
56
+ // ---- inside description strings, which spec-compliant JSON.parse rejects. ---
57
+ // ---- Walk the text; while inside a string literal, escape any bare control -
58
+ // ---- char (< 0x20) to its \uXXXX form. (Python needed strict=False here.) -
59
+ function sanitizeControlChars(text) {
60
+ let out = '';
61
+ let inString = false;
62
+ let escaped = false;
63
+ for (const ch of text) {
64
+ const code = ch.charCodeAt(0);
65
+ if (inString) {
66
+ if (escaped) {
67
+ out += ch;
68
+ escaped = false;
69
+ continue;
70
+ }
71
+ if (ch === '\\') {
72
+ out += ch;
73
+ escaped = true;
74
+ continue;
75
+ }
76
+ if (ch === '"') {
77
+ out += ch;
78
+ inString = false;
79
+ continue;
80
+ }
81
+ if (code < 0x20) {
82
+ out += '\\u' + code.toString(16).padStart(4, '0');
83
+ continue;
84
+ }
85
+ out += ch;
86
+ } else {
87
+ if (ch === '"') inString = true;
88
+ out += ch;
89
+ }
90
+ }
91
+ return out;
92
+ }
93
+
94
+ function parseBody(text) {
95
+ const body = extractBody(text);
96
+ if (!body) return { ok: false, data: null };
97
+ try {
98
+ return { ok: true, data: JSON.parse(body) };
99
+ } catch {
100
+ try {
101
+ return { ok: true, data: JSON.parse(sanitizeControlChars(body)) };
102
+ } catch {
103
+ return { ok: false, data: null };
104
+ }
105
+ }
106
+ }
107
+
108
+ const parsed = parseBody(clean);
109
+
110
+ // ---- detect the 403 self-heal signature and 404 route-missing --------------
111
+ const bodyText = clean;
112
+ const isFunctionalityNotEnabled = /FUNCTIONALITY_NOT_ENABLED/.test(bodyText);
113
+ const isNotFound = /NOT_FOUND/.test(bodyText);
114
+
115
+ function emit(obj) {
116
+ // Synchronous write to fd 1 — process.stdout.write() + process.exit(0) can
117
+ // truncate a large payload (availableNames on a big catalog) because exit
118
+ // races the async pipe drain.
119
+ writeSync(1, JSON.stringify(obj, null, 2) + '\n');
120
+ process.exit(0);
121
+ }
122
+
123
+ // ---- classify -------------------------------------------------------------
124
+ // 403 (explicit) OR a FUNCTIONALITY_NOT_ENABLED body with no 200 status.
125
+ if (httpStatus === 403 || (httpStatus === null && isFunctionalityNotEnabled && !parsed.ok)) {
126
+ emit({
127
+ httpStatus,
128
+ access: false,
129
+ catalogFetched: false,
130
+ action: 'SELF_HEAL',
131
+ recommendedOutcome: 'STOPPED_NO_ACCESS',
132
+ matchCount: null,
133
+ resolved: null,
134
+ availableNames: [],
135
+ note: '403 FUNCTIONALITY_NOT_ENABLED — assign UnifiedCatalogAdminPsl + UnifiedCatalogAdmin, then re-probe once. If still 403 after the heal, the org lacks the license (STOPPED_NO_ACCESS).',
136
+ });
137
+ }
138
+
139
+ if (httpStatus === 404 || (isNotFound && !parsed.ok)) {
140
+ emit({
141
+ httpStatus,
142
+ access: false,
143
+ catalogFetched: false,
144
+ action: 'STOP_ROUTE',
145
+ recommendedOutcome: 'STOPPED_OTHER',
146
+ matchCount: null,
147
+ resolved: null,
148
+ availableNames: [],
149
+ note: 'Route is below its minimum API version (needs v65.0+; this skill targets v67.0).',
150
+ });
151
+ }
152
+
153
+ // ---- any other explicit non-200 status (401 / 429 / 500 / 502 / …) ----------
154
+ // A parseable JSON error body (Salesforce errors are `[{errorCode,message}]`)
155
+ // must NOT be mistaken for an empty or missing catalog: an outage/auth/rate-limit
156
+ // failure is a real error, not "no such template". 403/404 are handled above; a
157
+ // null status (caller omitted -i) still falls through to body-shape detection.
158
+ if (httpStatus !== null && httpStatus !== 200) {
159
+ emit({
160
+ httpStatus,
161
+ access: false,
162
+ catalogFetched: false,
163
+ action: 'STOP_OTHER',
164
+ recommendedOutcome: 'STOPPED_OTHER',
165
+ matchCount: null,
166
+ resolved: null,
167
+ availableNames: [],
168
+ note: `Catalog GET returned HTTP ${httpStatus} — an API, auth, or transport failure, not an empty or missing catalog. Report the failure and stop; do not treat it as name-not-found.`,
169
+ });
170
+ }
171
+
172
+ if (!parsed.ok) {
173
+ emit({
174
+ httpStatus,
175
+ access: httpStatus === 200,
176
+ catalogFetched: false,
177
+ action: 'STOP_OTHER',
178
+ recommendedOutcome: 'STOPPED_OTHER',
179
+ matchCount: null,
180
+ resolved: null,
181
+ availableNames: [],
182
+ note: 'Could not parse a catalog body from the GET output.',
183
+ });
184
+ }
185
+
186
+ // ---- pull the template array from a catalog-SHAPED body ---------------------
187
+ // Accept only the canonical wrapper, the alternate wrapper, or a bare array whose
188
+ // entries look like templates (a string `name`). A parseable non-catalog body —
189
+ // e.g. an error array `[{errorCode,message}]` returned without a status line — is
190
+ // NOT catalog-shaped and must not be read as templates.
191
+ const wrapperArray =
192
+ (Array.isArray(parsed.data?.serviceProcessTemplateOutputRepresentation) && parsed.data.serviceProcessTemplateOutputRepresentation) ||
193
+ (Array.isArray(parsed.data?.serviceProcessTemplates) && parsed.data.serviceProcessTemplates) ||
194
+ null;
195
+ // Require length > 0: an EMPTY bare array vacuously passes `.every()`, so a
196
+ // null-status `[]` (caller omitted -i) would look catalog-shaped and fall through
197
+ // to STOP_EMPTY / STOPPED_NAME_NOT_FOUND — a definitive "no such template" — when
198
+ // the read may have failed. A genuinely-empty 200 catalog still reaches STOP_EMPTY
199
+ // below via the httpStatus===200 path; only the unconfirmed null-status `[]` is
200
+ // downgraded to the failed-read STOP_OTHER.
201
+ const bareCatalog =
202
+ Array.isArray(parsed.data) && parsed.data.length > 0 && parsed.data.every((t) => t && typeof t === 'object' && typeof t.name === 'string')
203
+ ? parsed.data
204
+ : null;
205
+ const catalogShaped = wrapperArray !== null || bareCatalog !== null;
206
+
207
+ // Parseable, but neither a 200 nor a catalog-shaped body (only reachable when the
208
+ // caller omitted -i so httpStatus is null): treat as a failed/unknown response,
209
+ // not an empty catalog.
210
+ if (!catalogShaped && httpStatus !== 200) {
211
+ emit({
212
+ httpStatus,
213
+ access: false,
214
+ catalogFetched: false,
215
+ action: 'STOP_OTHER',
216
+ recommendedOutcome: 'STOPPED_OTHER',
217
+ matchCount: null,
218
+ resolved: null,
219
+ availableNames: [],
220
+ note: 'The GET body is not a recognizable catalog response (and carried no HTTP 200 status) — report the failure and stop; do not treat it as an empty catalog.',
221
+ });
222
+ }
223
+
224
+ const templates = wrapperArray || bareCatalog || [];
225
+ const availableNames = templates.map((t) => t?.name).filter((n) => typeof n === 'string');
226
+
227
+ if (templates.length === 0) {
228
+ emit({
229
+ httpStatus: httpStatus ?? 200,
230
+ access: true,
231
+ catalogFetched: true,
232
+ action: 'STOP_EMPTY',
233
+ recommendedOutcome: 'STOPPED_NAME_NOT_FOUND',
234
+ matchCount: 0,
235
+ resolved: null,
236
+ availableNames: [],
237
+ note: 'Catalog returned an empty array — nothing to deploy.',
238
+ });
239
+ }
240
+
241
+ // No requested name — or a purely-whitespace one, which carries no name to
242
+ // resolve — → access confirmed, resolution deferred to the caller. The
243
+ // whitespace guard is load-bearing: without it a name like " " trims to ""
244
+ // below, and `String.includes("")` is true for every template, so `partial`
245
+ // would become the entire catalog and emit a bogus STOP_AMBIGUOUS.
246
+ if (!requestedName || !requestedName.trim()) {
247
+ emit({
248
+ httpStatus: httpStatus ?? 200,
249
+ access: true,
250
+ catalogFetched: true,
251
+ action: 'CONTINUE',
252
+ recommendedOutcome: null,
253
+ matchCount: null,
254
+ resolved: null,
255
+ availableNames,
256
+ note: 'Access confirmed. Re-run with a requestedName to resolve a specific template.',
257
+ });
258
+ }
259
+
260
+ // ---- case-insensitive exact-name match -------------------------------------
261
+ const wanted = requestedName.trim().toLowerCase();
262
+ const matches = templates.filter((t) => (t?.name ?? '').trim().toLowerCase() === wanted);
263
+
264
+ if (matches.length === 1) {
265
+ const m = matches[0];
266
+ emit({
267
+ httpStatus: httpStatus ?? 200,
268
+ access: true,
269
+ catalogFetched: true,
270
+ action: 'DEPLOY',
271
+ recommendedOutcome: null,
272
+ matchCount: 1,
273
+ resolved: {
274
+ id: m.id,
275
+ name: m.name,
276
+ templateDependencyMetadata: m.templateDependencyMetadata ?? [],
277
+ },
278
+ availableNames,
279
+ note: 'Exactly one match — proceed to build the deploy body from templateDependencyMetadata.',
280
+ });
281
+ }
282
+
283
+ if (matches.length >= 2) {
284
+ emit({
285
+ httpStatus: httpStatus ?? 200,
286
+ access: true,
287
+ catalogFetched: true,
288
+ action: 'STOP_AMBIGUOUS',
289
+ recommendedOutcome: 'STOPPED_NAME_AMBIGUOUS',
290
+ matchCount: matches.length,
291
+ resolved: null,
292
+ availableNames: matches.map((t) => t.name),
293
+ note: 'Two or more exact matches — stop and ask the user to pick the exact one. Never pick the first.',
294
+ });
295
+ }
296
+
297
+ // ---- zero exact matches: fall back to substring candidates ------------------
298
+ // A request can be a CATEGORY term ("access", "laptop") rather than a template
299
+ // name. If it appears inside two or more template names, the request is
300
+ // AMBIGUOUS, not simply not-found — the same "stop, list the candidates, ask for
301
+ // the exact one, never pick one" contract as a multi-exact tie. Only a term that
302
+ // matches zero (or a single near-match) template names is genuinely NOT_FOUND;
303
+ // a lone near-match is still never auto-deployed. matchCount carries the
304
+ // candidate count so the caller's STOPPED_NAME_AMBIGUOUS (>=2) stays coherent.
305
+ const partial = availableNames.filter((n) => n.toLowerCase().includes(wanted));
306
+
307
+ if (partial.length >= 2) {
308
+ emit({
309
+ httpStatus: httpStatus ?? 200,
310
+ access: true,
311
+ catalogFetched: true,
312
+ action: 'STOP_AMBIGUOUS',
313
+ recommendedOutcome: 'STOPPED_NAME_AMBIGUOUS',
314
+ matchCount: partial.length,
315
+ resolved: null,
316
+ availableNames: partial,
317
+ note: 'No exact match, but the requested term appears in two or more template names — the request is ambiguous. List these candidates and ask the user for the exact name. Never pick one.',
318
+ });
319
+ }
320
+
321
+ emit({
322
+ httpStatus: httpStatus ?? 200,
323
+ access: true,
324
+ catalogFetched: true,
325
+ action: 'STOP_NOT_FOUND',
326
+ recommendedOutcome: 'STOPPED_NAME_NOT_FOUND',
327
+ matchCount: 0,
328
+ resolved: null,
329
+ availableNames: partial.length ? partial : availableNames,
330
+ note: 'No exact match (and not enough near-matches to be ambiguous) — stop, report the requested name, and list available names. Never deploy a near-match.',
331
+ });