@salesforce/afv-skills 1.34.0 → 1.35.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 (67) hide show
  1. package/package.json +1 -1
  2. package/skills/automation-sandbox-post-copy-config-generate/SKILL.md +239 -0
  3. package/skills/automation-sandbox-post-copy-config-generate/assets/config_template.json +21 -0
  4. package/skills/automation-sandbox-post-copy-config-generate/assets/json_schema.json +90 -0
  5. package/skills/automation-sandbox-post-copy-config-generate/examples/sample_sop_excerpt.md +31 -0
  6. package/skills/automation-sandbox-post-copy-config-generate/examples/sample_sop_to_config.json +50 -0
  7. package/skills/automation-sandbox-post-copy-config-generate/references/configuration_catalog.md +76 -0
  8. package/skills/automation-sandbox-post-copy-config-generate/references/sop_parsing_patterns.md +157 -0
  9. package/skills/automation-sandbox-post-copy-config-generate/references/source_format_handling.md +230 -0
  10. package/skills/dx-apexguru-scan/SKILL.md +403 -0
  11. package/skills/dx-apexguru-scan/examples/README.md +54 -0
  12. package/skills/dx-apexguru-scan/examples/sample-decoded-summary.json +176 -0
  13. package/skills/dx-apexguru-scan/examples/sample-full-no-runtime-response.json +26 -0
  14. package/skills/dx-apexguru-scan/examples/sample-succeeded-response.json +15 -0
  15. package/skills/dx-apexguru-scan/references/api-reference.md +81 -0
  16. package/skills/dx-apexguru-scan/references/authentication.md +134 -0
  17. package/skills/dx-apexguru-scan/references/error-handling.md +56 -0
  18. package/skills/dx-apexguru-scan/references/violation-catalog.md +28 -0
  19. package/skills/dx-apexguru-scan/scripts/build-zip.sh +87 -0
  20. package/skills/dx-apexguru-scan/scripts/decode-report.js +389 -0
  21. package/skills/dx-apexguru-scan/scripts/resolve-token.sh +151 -0
  22. package/skills/dx-apexguru-scan/scripts/run-scan.sh +153 -0
  23. package/skills/dx-apexguru-scan/scripts/scan.sh +96 -0
  24. package/skills/dx-apexguru-scan/scripts/validate-token.js +121 -0
  25. package/skills/dx-devops-pipeline-manage/SKILL.md +263 -0
  26. package/skills/dx-devops-pipeline-manage/examples/common-workflows.md +177 -0
  27. package/skills/dx-devops-pipeline-manage/references/cli-commands.md +298 -0
  28. package/skills/dx-devops-pipeline-manage/references/parsing-patterns.md +134 -0
  29. package/skills/dx-devops-pipeline-manage/scripts/check-activation-ready.sh +34 -0
  30. package/skills/dx-devops-pipeline-manage/scripts/validate-org-type.sh +17 -0
  31. package/skills/dx-devops-pipeline-manage/scripts/verify-operation.sh +82 -0
  32. package/skills/dx-devops-promote/SKILL.md +214 -0
  33. package/skills/dx-devops-promote/examples/promotion-workflows.md +212 -0
  34. package/skills/dx-devops-promote/references/cli-commands.md +303 -0
  35. package/skills/experience-lwc-base-components-integrate/SKILL.md +176 -0
  36. package/skills/experience-lwc-base-components-integrate/references/lbc-expert-guidance.md +127 -0
  37. package/skills/experience-lwc-base-components-integrate/references/lightning-component-index.md +179 -0
  38. package/skills/experience-lwc-base-components-integrate/references/lightning-components.md +5429 -0
  39. package/skills/experience-lwc-base-components-integrate/scripts/extract-component-docs.sh +61 -0
  40. package/skills/experience-lwc-rtl-validate/SKILL.md +149 -0
  41. package/skills/experience-lwc-rtl-validate/references/rtl-expert.md +892 -0
  42. package/skills/experience-lwc-rtl-validate/scripts/scan-rtl-css.sh +206 -0
  43. package/skills/experience-lwc-typescript-migrate/SKILL.md +207 -0
  44. package/skills/experience-lwc-typescript-migrate/assets/dts-template.ts +15 -0
  45. package/skills/experience-lwc-typescript-migrate/assets/type-patterns.ts +44 -0
  46. package/skills/experience-lwc-typescript-migrate/scripts/find-consumers.sh +128 -0
  47. package/skills/experience-ui-bundle-localize/SKILL.md +323 -0
  48. package/skills/experience-ui-bundle-localize/references/gotchas.md +249 -0
  49. package/skills/experience-ui-bundle-localize/references/i18n-setup.md +169 -0
  50. package/skills/experience-ui-bundle-localize/references/interpolation.md +311 -0
  51. package/skills/experience-ui-bundle-localize/references/label-xml.md +282 -0
  52. package/skills/experience-ui-bundle-localize/references/verifying.md +219 -0
  53. package/skills/experience-ui-bundle-localize/scripts/check-i18n-wired.sh +195 -0
  54. package/skills/experience-ui-bundle-localize/scripts/check-manifest-registered.sh +100 -0
  55. package/skills/experience-ui-bundle-localize/scripts/check-org-api-version.sh +40 -0
  56. package/skills/experience-ui-bundle-localize/scripts/detect-bundle-type.sh +57 -0
  57. package/skills/platform-custom-lightning-type-generate/SKILL.md +3 -0
  58. package/skills/platform-custom-lightning-type-generate/assets/primitive-types-and-constraints.md +1 -1
  59. package/skills/platform-mcp-tool-widget-coordinate/SKILL.md +250 -0
  60. package/skills/platform-mcp-tool-widget-coordinate/examples/action-name-source-prompt.md +74 -0
  61. package/skills/platform-mcp-tool-widget-coordinate/examples/apex-invocable-source-prompt.md +90 -0
  62. package/skills/platform-mcp-tool-widget-coordinate/examples/nested-object-source-prompt.md +191 -0
  63. package/skills/platform-mcp-tool-widget-coordinate/examples/pasted-tool-output-prompt.md +85 -0
  64. package/skills/platform-mcp-tool-widget-coordinate/references/build-plan-format.md +74 -0
  65. package/skills/platform-mcp-tool-widget-coordinate/references/mcp-tool-output-discovery.md +184 -0
  66. package/skills/platform-mcp-tool-widget-coordinate/references/two-clt-modeling.md +128 -0
  67. package/skills/platform-mcp-tool-widget-coordinate/references/validation-gates.md +181 -0
@@ -0,0 +1,389 @@
1
+ #!/usr/bin/env node
2
+ // Decode the base64 `report` field from an ApexGuru SFAP Scan SUCCEEDED body
3
+ // and summarize violations grouped by rule. This is the ONLY sanctioned way to
4
+ // read the report — never inline base64/jq/Read against the raw result file.
5
+ //
6
+ // Usage:
7
+ // node decode-report.js <raw-result.json> [--group rule|severity|file]
8
+ // [--rule RULE] [--severity S]
9
+ // [--file NAME] [--top N] [--full]
10
+ // [--present]
11
+ //
12
+ // Default: prints a single-line JSON summary to stdout:
13
+ // { analysisMode, attribution, violationCount, filesScanned, severityCounts,
14
+ // groups:[{key,count,severityCounts,sample:[...]}], topViolations:[...],
15
+ // truncated }
16
+ // With --full, `groups[].items` holds every violation in that group.
17
+ //
18
+ // With --present: renders ready-to-show markdown directly (severity legend,
19
+ // one "### Issue N" card per violation with message/code/fix/resource, a
20
+ // collapsed "CPU Hotspots" table for the ExpensiveMethods rule, and a closing
21
+ // "## Summary" table listing every violation) instead of dumping raw JSON.
22
+ // Implies --full (no silent caps in a response meant to be presented as complete).
23
+
24
+ const fs = require("fs");
25
+
26
+ function fail(msg) {
27
+ console.error(msg);
28
+ process.exit(1);
29
+ }
30
+
31
+ const args = process.argv.slice(2);
32
+ if (args.length < 1) fail("Usage: node decode-report.js <raw-result.json> [options]");
33
+
34
+ const rawPath = args[0];
35
+ const opts = { group: "rule", top: 10, full: false, rule: null, severity: null, file: null, present: false };
36
+ for (let i = 1; i < args.length; i++) {
37
+ const a = args[i];
38
+ if (a === "--group") opts.group = args[++i];
39
+ else if (a === "--top") opts.top = parseInt(args[++i], 10) || 10;
40
+ else if (a === "--full") opts.full = true;
41
+ else if (a === "--present") { opts.present = true; opts.full = true; }
42
+ else if (a === "--rule") opts.rule = args[++i];
43
+ else if (a === "--severity") opts.severity = String(args[++i]);
44
+ else if (a === "--file") opts.file = args[++i];
45
+ }
46
+
47
+ let raw;
48
+ try {
49
+ raw = JSON.parse(fs.readFileSync(rawPath, "utf8"));
50
+ } catch (e) {
51
+ fail(`Could not read/parse raw result file: ${e.message}`);
52
+ }
53
+
54
+ if (raw.status && raw.status !== "SUCCEEDED") {
55
+ fail(`Scan status is ${raw.status}, not SUCCEEDED — nothing to decode. Message: ${raw.message || "(none)"}`);
56
+ }
57
+
58
+ // --- decode the base64 report into a violations array ---
59
+ let violations = [];
60
+ if (raw.report) {
61
+ let decoded;
62
+ try {
63
+ decoded = Buffer.from(raw.report, "base64").toString("utf8");
64
+ } catch (e) {
65
+ fail(`report field is not valid base64: ${e.message}`);
66
+ }
67
+ let parsed;
68
+ try {
69
+ parsed = JSON.parse(decoded);
70
+ } catch (e) {
71
+ fail(`decoded report is not valid JSON: ${e.message}`);
72
+ }
73
+ // API contract: report is a JSON array of violations. Tolerate a wrapper obj.
74
+ violations = Array.isArray(parsed) ? parsed : parsed.violations || [];
75
+ }
76
+
77
+ // --- normalize each violation to a stable shape (tolerate field-name drift) ---
78
+ function loc(v) {
79
+ const l = v.location || (Array.isArray(v.locations) && v.locations[0]) || {};
80
+ return {
81
+ file: basename(l.file || v.file || "unknown"),
82
+ line: l.line || l.startLine || v.line || 0,
83
+ comment: l.comment || "",
84
+ };
85
+ }
86
+ function basename(p) {
87
+ return String(p).split(/[\\/]/).pop();
88
+ }
89
+ // The API's real field for fix code is `suggestions: [{message, location}]` —
90
+ // each suggestion's `message` IS the replacement code. Older/hypothetical
91
+ // shapes (fixes/suggestedFixes/suggestedFix) are tolerated as a fallback.
92
+ function fixesOf(v) {
93
+ if (Array.isArray(v.suggestions) && v.suggestions.length) {
94
+ return v.suggestions.map((s) => (typeof s === "string" ? s : s.message)).filter(Boolean);
95
+ }
96
+ const f = v.fixes || v.suggestedFixes || (v.suggestedFix ? [v.suggestedFix] : []);
97
+ return (Array.isArray(f) ? f : [f]).filter(Boolean);
98
+ }
99
+ // `locations[].comment` holds "ClassName.methodName" — split for a display name.
100
+ function methodOf(comment) {
101
+ if (!comment) return "";
102
+ const parts = comment.split(".");
103
+ return parts.length > 1 ? parts.slice(1).join(".") : comment;
104
+ }
105
+ // The API sometimes prefixes each line of `original_code` with "Line N:" —
106
+ // strip that so the snippet reads as plain Apex inside a code block.
107
+ function stripLinePrefixes(code) {
108
+ return code.replace(/^Line \d+:\s?/gm, "").replace(/\n+$/, "");
109
+ }
110
+ function norm(v) {
111
+ const { file, line, comment } = loc(v);
112
+ const metadata = v.metadata || {};
113
+ return {
114
+ rule: v.rule || v.ruleName || v.type || "UNKNOWN",
115
+ message: v.message || v.description || "",
116
+ severity: String(v.severity ?? v.sev ?? ""),
117
+ file,
118
+ line,
119
+ method: methodOf(comment),
120
+ originalCode: metadata.original_code ? stripLinePrefixes(metadata.original_code) : "",
121
+ cpuTimePercentage: typeof metadata.cpu_time_percentage === "number" ? metadata.cpu_time_percentage : null,
122
+ fixes: fixesOf(v),
123
+ resources: v.resources || v.helpDocs || v.help || [],
124
+ };
125
+ }
126
+ const allItems = violations.map(norm);
127
+ let items = allItems;
128
+
129
+ // --- optional filters ---
130
+ if (opts.rule) items = items.filter((v) => v.rule === opts.rule);
131
+ if (opts.severity) items = items.filter((v) => v.severity === opts.severity);
132
+ if (opts.file) items = items.filter((v) => v.file === opts.file || v.file.includes(opts.file));
133
+
134
+ // --- aggregates ---
135
+ const severityCounts = {};
136
+ for (const v of items) severityCounts[v.severity || "?"] = (severityCounts[v.severity || "?"] || 0) + 1;
137
+
138
+ function keyOf(v) {
139
+ return opts.group === "severity" ? (v.severity || "?") : opts.group === "file" ? v.file : v.rule;
140
+ }
141
+ const byKey = new Map();
142
+ for (const v of items) {
143
+ const k = keyOf(v);
144
+ if (!byKey.has(k)) byKey.set(k, []);
145
+ byKey.get(k).push(v);
146
+ }
147
+ const groups = [...byKey.entries()]
148
+ .map(([key, arr]) => {
149
+ const sc = {};
150
+ for (const v of arr) sc[v.severity || "?"] = (sc[v.severity || "?"] || 0) + 1;
151
+ const g = { key, count: arr.length, severityCounts: sc, sample: arr.slice(0, 3) };
152
+ if (opts.full) g.items = arr;
153
+ return g;
154
+ })
155
+ .sort((a, b) => b.count - a.count);
156
+
157
+ // Top violations by severity (numeric asc = most severe first when sev is 1..5).
158
+ const topViolations = items
159
+ .slice()
160
+ .sort((a, b) => (parseInt(a.severity, 10) || 99) - (parseInt(b.severity, 10) || 99) || a.rule.localeCompare(b.rule))
161
+ .slice(0, opts.top);
162
+
163
+ // full = enriched with runtime metrics; static = source-only. Onboarded orgs with
164
+ // no runtime data for this code yet come back full but static-equivalent (no
165
+ // cpu_time_percentage), so only call it "Production insights" when metrics exist.
166
+ // Compute this from the FULL (unfiltered) violation list — attribution is a
167
+ // property of the whole scan, so a --rule/--severity/--file drill-down must not
168
+ // flip an enriched scan to "Static only" just because the selected subset has
169
+ // no cpu_time_percentage.
170
+ const analysisMode = raw.analysisMode || "static";
171
+ const hasRuntimeMetrics = allItems.some((v) => v.cpuTimePercentage != null);
172
+ const attribution = analysisMode === "full" && hasRuntimeMetrics ? "Production insights" : "Static only";
173
+
174
+ const summary = {
175
+ scanId: raw.scanId || null,
176
+ analysisMode,
177
+ attribution,
178
+ violationCount: items.length,
179
+ filesScanned: raw.filesScanned ?? null,
180
+ serverViolationBreakdown: raw.violationBreakdown || null,
181
+ severityCounts,
182
+ groupedBy: opts.group,
183
+ groups: opts.full ? groups : groups.slice(0, opts.top),
184
+ topViolations,
185
+ truncated: !opts.full && groups.length > opts.top,
186
+ };
187
+
188
+ if (!opts.present) {
189
+ console.log(JSON.stringify(summary));
190
+ process.exit(0);
191
+ }
192
+
193
+ // --- --present: render ready-to-read markdown (icons, per-issue detail, ---
194
+ // --- summary table) instead of leaving table-building up to the reader. ---
195
+
196
+ // ApexGuru severities run 1 (critical) .. 5 (info); map to icon + label.
197
+ const SEVERITY_MAP = {
198
+ 1: { icon: "\u{1F534}", label: "Critical" }, // red
199
+ 2: { icon: "\u{1F7E0}", label: "Major" }, // orange
200
+ 3: { icon: "\u{1F7E0}", label: "Major" }, // orange
201
+ 4: { icon: "\u{1F7E1}", label: "Minor" }, // yellow
202
+ 5: { icon: "\u{1F7E1}", label: "Minor" }, // yellow
203
+ };
204
+ function sevInfo(sev) {
205
+ return SEVERITY_MAP[parseInt(sev, 10)] || { icon: "⚪", label: `Severity ${sev}` };
206
+ }
207
+
208
+ // Human-friendly rule names mapping (for violation.rule field)
209
+ const RULE_DISPLAY_NAMES = {
210
+ // Single-file scan rules (API_SUPPORTED_AP_TYPES)
211
+ "SoqlWithoutAWhereClauseOrLimitStatement": "SOQL Without WHERE Clause or LIMIT Statement",
212
+ "SoqlWithUnusedFields": "SOQL With Unused Fields",
213
+ "SoqlWithWildcardFilter": "SOQL With Wildcard Filter",
214
+ "SchemaGetGlobalDescribeNotEfficient": "Inefficient Schema.getGlobalDescribe() Usage",
215
+ "SoqlInALoop": "SOQL in Loop",
216
+ "SoqlInALoopOneHop": "SOQL in One-Hop Loop",
217
+ "DmlInALoop": "DML in Loop",
218
+ "SortingInApex": "Sorting in Apex",
219
+ "BusyLoopDelay": "Busy Loop Delay",
220
+ "CopyingListOrSetElementsUsingAForLoop": "Copying List/Set Elements Using a For Loop",
221
+ "SObjectMapInAForLoop": "SObject Map Lookup in a Loop",
222
+ "SoqlWithNegativeExpressions": "SOQL With Negative Expressions",
223
+ "SoqlWithApexFilter": "SOQL With Apex Filter",
224
+ "ExpensiveMethods": "Expensive Methods",
225
+ "UsingTheTestMethodKeyword": "Using the testMethod Keyword",
226
+ "WritingFillerStatements": "Writing Filler Statements",
227
+ // Project-scope scan rules (API_SCAN_PROJECT_AP_TYPES)
228
+ "UnusedMethods": "Unused Methods",
229
+ "LimitsGetHeapsizeMethods": "Limits.getHeapSize() in Loop",
230
+ "ExpensiveStringComparison": "Expensive String Comparison",
231
+ "ExpensiveDebugStatements": "Expensive Debug Statements",
232
+ // Fallback-style entries (no explicit ANTI_PATTERN_RULE_NAMES mapping)
233
+ "Soql Aggregation": "SOQL Aggregation",
234
+ "Redundant Soql": "Redundant SOQL",
235
+ };
236
+
237
+ // Mapping for violationBreakdown keys (SCREAMING_SNAKE_CASE internal IDs)
238
+ const BREAKDOWN_DISPLAY_NAMES = {
239
+ "SOQL_NO_WHERE_LIMIT": "SOQL Without WHERE Clause or LIMIT Statement",
240
+ "SOQL_UNUSED_FIELDS": "SOQL With Unused Fields",
241
+ "SOQL_WILDCARD": "SOQL With Wildcard Filter",
242
+ "GGD": "Inefficient Schema.getGlobalDescribe() Usage",
243
+ "SOQL_IN_LOOP": "SOQL in Loop",
244
+ "SOQL_IN_LOOP_1HOP": "SOQL in One-Hop Loop",
245
+ "DML_IN_LOOP": "DML in Loop",
246
+ "SOQL_SORT_IN_APEX": "Sorting in Apex",
247
+ "BUSY_DELAY_LOOP": "Busy Loop Delay",
248
+ "COPY_LIST_ELEMENTS_LOOP": "Copying List/Set Elements Using a For Loop",
249
+ "CREATE_MAP_WITH_LOOP": "SObject Map Lookup in a Loop",
250
+ "SOQL_NEGATIVE_EXPR": "SOQL With Negative Expressions",
251
+ "SOQL_FILTER_IN_APEX": "SOQL With Apex Filter",
252
+ "EXPENSIVE_METHODS": "Expensive Methods",
253
+ "DEPRECATED_TEST_METHOD_KEYWORD": "Using the testMethod Keyword",
254
+ "CODE_COVERAGE_INFLATION": "Writing Filler Statements",
255
+ "OBSOLETE_CLASSES_METHODS": "Unused Methods",
256
+ "EXPENSIVE_LIMIT_METHODS": "Limits.getHeapSize() in Loop",
257
+ "EXPENSIVE_STRING_COMPARE": "Expensive String Comparison",
258
+ "EXPENSIVE_DEBUG_STMT": "Expensive Debug Statements",
259
+ "SOQL_AGGREGATION": "SOQL Aggregation",
260
+ "REDUNDANT_SOQL": "Redundant SOQL",
261
+ };
262
+ function displayRuleName(rule) {
263
+ return RULE_DISPLAY_NAMES[rule] || rule;
264
+ }
265
+
266
+ // Three ApexGuru states, distinguished per the SFAP contract (hasRuntimeMetrics
267
+ // and attribution computed above):
268
+ // analysisMode: static → org is NOT onboarded to ApexGuru
269
+ // analysisMode: full + runtime data → onboarded; runtime metrics applied
270
+ // analysisMode: full + NO runtime → onboarded, but no runtime data for this
271
+ // code yet (result is static-equivalent)
272
+ const isFull = summary.analysisMode === "full";
273
+ const isEnriched = isFull && hasRuntimeMetrics;
274
+ const isOnboardedNoData = isFull && !hasRuntimeMetrics;
275
+
276
+ const violationWord = summary.violationCount === 1 ? "violation" : "violations";
277
+ // attribution already reflects runtime presence ("Production insights" only when enriched).
278
+ let out = `# ApexGuru Scan Results — ${summary.attribution}\n\n`;
279
+ out += `Found **${summary.violationCount} performance ${violationWord}** across ${summary.filesScanned ?? "?"} file(s). `;
280
+ if (isEnriched) {
281
+ out += "Findings enriched with production runtime metrics.\n";
282
+ } else if (isOnboardedNoData) {
283
+ out += "No runtime metrics were found for this class yet. Generate an ApexGuru report in Scale Center to see runtime insights.\n";
284
+ } else {
285
+ out += "ApexGuru static analysis is active. To see how this code performs in your production org, generate a runtime report in Scale Center.\n";
286
+ }
287
+ out += "\n";
288
+
289
+ out += "## Severity Legend\n\n";
290
+ out += "- \u{1F534} Critical — blocks deployment and causes test failures\n";
291
+ out += "- \u{1F7E0} Major — reduces reliability without blocking deployment\n";
292
+ out += "- \u{1F7E1} Minor — deviates from quality standards\n";
293
+ if (isEnriched) out += "- \u{1F4A1} Severity adjusted based on production performance.\n";
294
+ out += "\n";
295
+
296
+ if (summary.violationCount === 0) {
297
+ out += "No performance antipatterns found.\n";
298
+ console.log(out);
299
+ process.exit(0);
300
+ }
301
+
302
+ const allViolations = groups.flatMap((g) => g.items || g.sample);
303
+ allViolations.sort((a, b) => (parseInt(a.severity, 10) || 99) - (parseInt(b.severity, 10) || 99));
304
+
305
+ // ExpensiveMethods is a per-method CPU-hotspot ranking, not a line-level
306
+ // antipattern — with N methods ranked it produces N near-identical cards, so
307
+ // collapse it into one ranked table instead of repeating the detail format.
308
+ const hotspots = allViolations.filter((v) => v.rule === "ExpensiveMethods");
309
+ const otherViolations = allViolations.filter((v) => v.rule !== "ExpensiveMethods");
310
+
311
+ if (hotspots.length) {
312
+ out += `## CPU Hotspots (${hotspots.length})\n\n`;
313
+ out += "Methods ranked by share of observed Apex CPU time (production metrics):\n\n";
314
+ out += "| Method | % of CPU time |\n";
315
+ out += "|--------|---------------|\n";
316
+ hotspots
317
+ .slice()
318
+ .sort((a, b) => (b.cpuTimePercentage || 0) - (a.cpuTimePercentage || 0))
319
+ .forEach((v) => {
320
+ const pct = v.cpuTimePercentage != null ? Math.round(v.cpuTimePercentage * 10) / 10 : "?";
321
+ out += `| \`${v.method || v.file}\` | ${pct}% |\n`;
322
+ });
323
+ out += "\n";
324
+ }
325
+
326
+ // Full-detail cards for the remaining (non-hotspot) violations, capped at
327
+ // 10 most critical. Both the detailed cards AND the summary table are capped.
328
+ const cardLimit = 10;
329
+ const cards = otherViolations.slice(0, cardLimit);
330
+ const remainingCount = otherViolations.length - cardLimit;
331
+
332
+ if (cards.length) {
333
+ out += `## Issues${otherViolations.length > cardLimit ? ` (top ${cardLimit} of ${otherViolations.length}, most critical first)` : ""}\n\n`;
334
+ cards.forEach((v, i) => {
335
+ const sev = sevInfo(v.severity);
336
+ const where = v.method ? ` in \`${v.method}\`` : "";
337
+ const ruleName = displayRuleName(v.rule);
338
+ out += `### Issue ${i + 1} — ${isEnriched ? "\u{1F4A1}" : ""}${sev.icon} ${sev.label}: ${ruleName}${where} (Line ${v.line})\n\n`;
339
+ out += `${v.message}\n\n`;
340
+ if (v.originalCode) {
341
+ out += `**Current code** (\`${v.file}\`, line ${v.line}):\n\n\`\`\`apex\n${v.originalCode}\n\`\`\`\n\n`;
342
+ }
343
+ if (v.fixes.length) {
344
+ out += `**Suggested fix:**\n\n\`\`\`apex\n${v.fixes[0]}\n\`\`\`\n\n`;
345
+ }
346
+ if (v.resources.length) {
347
+ out += `[Learn more](${v.resources[0]})\n\n`;
348
+ }
349
+ });
350
+ }
351
+
352
+ // Summary table also capped at top 10 (matches the detailed cards above).
353
+ out += "## Summary\n\n";
354
+ out += "| # | Severity | Rule | Method | Line |\n";
355
+ out += "|---|----------|------|--------|------|\n";
356
+ const summaryViolations = allViolations.slice(0, cardLimit);
357
+ summaryViolations.forEach((v, i) => {
358
+ const sev = sevInfo(v.severity);
359
+ const lineCell = v.rule === "ExpensiveMethods" ? "-" : v.line;
360
+ const ruleName = displayRuleName(v.rule);
361
+ out += `| ${i + 1} | ${sev.icon} ${sev.label} | ${ruleName} | ${v.method || "-"} | ${lineCell} |\n`;
362
+ });
363
+
364
+ // Add Anti-pattern breakdown section with summary (always show the full breakdown)
365
+ if (summary.serverViolationBreakdown && Object.keys(summary.serverViolationBreakdown).length > 0) {
366
+ out += `\n---\n\n`;
367
+ const breakdown = summary.serverViolationBreakdown;
368
+ const ruleTypeCount = Object.keys(breakdown).length;
369
+
370
+ // Summary line
371
+ out += `Detected **${summary.violationCount} anti-patterns** across **${ruleTypeCount} rule types**.\n\n`;
372
+
373
+ // Breakdown list
374
+ out += `**Anti-pattern breakdown:**\n\n`;
375
+ const sortedBreakdown = Object.entries(breakdown).sort((a, b) => b[1] - a[1]);
376
+ sortedBreakdown.forEach(([key, count]) => {
377
+ const displayName = BREAKDOWN_DISPLAY_NAMES[key] || key.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase());
378
+ out += `- ${displayName} (${count})\n`;
379
+ });
380
+
381
+ // Footer message if more than 10 violations
382
+ if (remainingCount > 0) {
383
+ out += `\n---\n\n`;
384
+ out += `Showing the top ${cardLimit} of ${otherViolations.length} antipatterns. `;
385
+ out += `Type **"show all"** or ask for details to see the remaining ${remainingCount}.\n`;
386
+ }
387
+ }
388
+
389
+ console.log(out);
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env bash
2
+ # Resolve the SFAP access token (JWT, sfap_api scope) and API base URL for the
3
+ # ApexGuru SFAP Scan API, then print them as a single JSON object to stdout.
4
+ #
5
+ # Usage: bash resolve-token.sh [--org <alias>]
6
+ #
7
+ # The base URL is derived from the token's own environment (prod/stage/dev),
8
+ # detected from its tnk claim by validate-token.js: prod → api.salesforce.com,
9
+ # stage → stage.api.salesforce.com, dev → dev.api.salesforce.com. So a prod org
10
+ # hits prod and an internal dev/scratch org hits dev, with no extra config.
11
+ #
12
+ # Output (stdout, single line JSON): {"baseUrl":"...","tokenFile":"...","source":"..."}
13
+ # The SFAP JWT is a secret, so it is NEVER printed to stdout. Instead it is
14
+ # written to a caller-owned 0600 temp file and the PATH is returned in
15
+ # `tokenFile`; the caller (run-scan.sh) reads it into memory and deletes the
16
+ # file immediately. Only non-secret fields (baseUrl/source/env) go to stdout.
17
+ # On failure: prints a JSON object {"error":"...","hint":"..."} and exits 1.
18
+ #
19
+ # Token resolution order (first hit wins):
20
+ # 1. APEXGURU_SFAP_TOKEN env var — the raw JWT (most portable; CI/headless)
21
+ # 2. APEXGURU_SFAP_TOKEN_FILE env var — path to a file holding the JWT
22
+ # 3. sf CLI + <instanceUrl>/ide/auth — derive the JWT from an authenticated org
23
+ # (per Tharun's interim guide). Org alias from --org or APEXGURU_SF_ORG;
24
+ # omit to use the CLI's default/target org.
25
+ #
26
+ # NOTE: the /ide/auth derivation (3) is Tharun's INTERIM approach; the Code
27
+ # Analyzer team may provide a cleaner path later. This script is the ONLY place
28
+ # that changes when it does — everything downstream consumes the token opaquely.
29
+ set -euo pipefail
30
+
31
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
32
+
33
+ # Per-environment ApexGuru API hosts. The one used is chosen from the token's
34
+ # detected env below, so the endpoint always matches the org the token came from.
35
+ PROD_HOST="api.salesforce.com"
36
+ STAGE_HOST="stage.api.salesforce.com"
37
+ DEV_HOST="dev.api.salesforce.com"
38
+ API_PATH="platform/scale/v1-beta.1/apex-guru"
39
+
40
+ ORG_ALIAS="${APEXGURU_SF_ORG:-}"
41
+ while [ $# -gt 0 ]; do
42
+ case "$1" in
43
+ --org) ORG_ALIAS="${2:-}"; shift 2 ;;
44
+ *) shift ;;
45
+ esac
46
+ done
47
+
48
+ TOKEN=""
49
+ SOURCE=""
50
+
51
+ # Priority 1: explicit env var (most portable; works headless / in CI).
52
+ if [ -n "${APEXGURU_SFAP_TOKEN:-}" ]; then
53
+ TOKEN="${APEXGURU_SFAP_TOKEN}"
54
+ SOURCE="env:APEXGURU_SFAP_TOKEN"
55
+ fi
56
+
57
+ # Priority 2: a token file the user points us at (avoids leaking into shell history).
58
+ if [ -z "$TOKEN" ] && [ -n "${APEXGURU_SFAP_TOKEN_FILE:-}" ] && [ -f "${APEXGURU_SFAP_TOKEN_FILE}" ]; then
59
+ TOKEN="$(tr -d '[:space:]' < "${APEXGURU_SFAP_TOKEN_FILE}")"
60
+ SOURCE="file:${APEXGURU_SFAP_TOKEN_FILE}"
61
+ fi
62
+
63
+ # Priority 3: derive the JWT from an authenticated sf CLI org via <instanceUrl>/ide/auth
64
+ # (Tharun's interim guide). Needs `sf` + `jq`. Errors here are non-fatal — we fall
65
+ # through to the "no token" message below so the env-var paths still work standalone.
66
+ #
67
+ # IMPORTANT: `sf org display --json` REDACTS `accessToken` (CLI >= ~2.14x), so it
68
+ # CANNOT be used as the session token — sending it yields a bare 401 "No session
69
+ # ID sent". The live, unredacted session id is obtained instead from a frontdoor
70
+ # URL (`sf org open --url-only`): visiting it sets a `sid` cookie, which /ide/auth
71
+ # accepts as a Bearer token and exchanges for the SFAP JWT. The sid is a secret —
72
+ # it lives only in a 0600 temp cookie jar that we delete on exit; never echoed.
73
+ if [ -z "$TOKEN" ] && command -v sf >/dev/null 2>&1 && command -v jq >/dev/null 2>&1 && command -v curl >/dev/null 2>&1; then
74
+ ORG_ARGS=()
75
+ [ -n "$ORG_ALIAS" ] && ORG_ARGS=(--target-org "$ORG_ALIAS")
76
+
77
+ # instanceUrl is NOT redacted; grab it (accessToken from this call is useless — redacted).
78
+ # ${ORG_ARGS[@]+...} guards empty-array expansion under `set -u` on bash 3.2 (macOS).
79
+ INSTANCE_URL=""
80
+ if ORG_JSON="$(sf org display ${ORG_ARGS[@]+"${ORG_ARGS[@]}"} --json 2>/dev/null)"; then
81
+ INSTANCE_URL="$(printf '%s' "$ORG_JSON" | jq -r '.result.instanceUrl // empty')"
82
+ fi
83
+
84
+ # frontdoor URL carries a one-time pad; following it mints the real `sid` cookie.
85
+ FRONTDOOR_URL=""
86
+ if OPEN_JSON="$(sf org open ${ORG_ARGS[@]+"${ORG_ARGS[@]}"} --url-only --json 2>/dev/null)"; then
87
+ FRONTDOOR_URL="$(printf '%s' "$OPEN_JSON" | jq -r '.result.url // empty')"
88
+ fi
89
+
90
+ if [ -n "$INSTANCE_URL" ] && [ -n "$FRONTDOOR_URL" ]; then
91
+ COOKIE_JAR="$(mktemp "${TMPDIR:-/tmp}/apexguru-sid.XXXXXX")"
92
+ chmod 600 "$COOKIE_JAR"
93
+ trap 'rm -f "$COOKIE_JAR"' EXIT
94
+ # Follow the frontdoor to capture the session cookie; discard the HTML body.
95
+ curl -sS -c "$COOKIE_JAR" -L -o /dev/null "$FRONTDOOR_URL" 2>/dev/null || true
96
+ SID="$(awk -F'\t' 'tolower($6)=="sid"{print $7}' "$COOKIE_JAR" 2>/dev/null | head -1)"
97
+
98
+ if [ -n "$SID" ]; then
99
+ # Exchange the session id for the SFAP JWT. Response: {"jwt":"...","message":"..."}.
100
+ AUTH_JSON="$(curl -sS "${INSTANCE_URL%/}/ide/auth" -H "Authorization: Bearer $SID" 2>/dev/null || true)"
101
+ CANDIDATE="$(printf '%s' "$AUTH_JSON" | jq -r '.jwt // empty' 2>/dev/null || true)"
102
+ if [ -n "$CANDIDATE" ] && [ "$CANDIDATE" != "null" ]; then
103
+ TOKEN="$CANDIDATE"
104
+ SOURCE="sf-cli:/ide/auth${ORG_ALIAS:+ (org=$ORG_ALIAS)}"
105
+ fi
106
+ fi
107
+ rm -f "$COOKIE_JAR"; trap - EXIT
108
+ fi
109
+ fi
110
+
111
+ if [ -z "$TOKEN" ]; then
112
+ printf '{"error":"no SFAP token found","hint":"Sign in to an authorized Salesforce org to run an ApexGuru scan and compare your code against production performance data."}\n'
113
+ exit 1
114
+ fi
115
+
116
+ # Local sanity check (no network): catch the two things that cause almost every
117
+ # 401 — missing sfap_api scope, expired — and detect the token's environment
118
+ # BEFORE we zip/upload. The token is piped on stdin (never argv) so it stays out
119
+ # of the process list. Only provable failures block; anything unreadable is a
120
+ # non-fatal warning.
121
+ DETECTED_ENV=""
122
+ if command -v node >/dev/null 2>&1; then
123
+ if ! VALIDATION="$(printf '%s' "$TOKEN" | node "$SCRIPT_DIR/validate-token.js")"; then
124
+ # validate-token.js already printed {"ok":false,"error","hint"} to stdout.
125
+ echo "$VALIDATION"
126
+ exit 1
127
+ fi
128
+ DETECTED_ENV="$(printf '%s' "$VALIDATION" | jq -r '.detectedEnv // empty' 2>/dev/null || true)"
129
+ fi
130
+
131
+ # Route to the host matching the token's env; default to prod when undetectable.
132
+ case "$DETECTED_ENV" in
133
+ dev) API_HOST="$DEV_HOST" ;;
134
+ stage) API_HOST="$STAGE_HOST" ;;
135
+ *) API_HOST="$PROD_HOST" ;;
136
+ esac
137
+ BASE_URL="https://${API_HOST}/${API_PATH}"
138
+
139
+ # The token is a secret: write it to a 0600 temp file and return only the PATH,
140
+ # never the token itself. This keeps it out of stdout (which is visible to the
141
+ # agent when this script is invoked directly, per the skill's script index).
142
+ # The caller reads the file and deletes it. `printf` avoids a trailing newline.
143
+ TOKEN_FILE="$(mktemp "${TMPDIR:-/tmp}/apexguru-jwt.XXXXXX")"
144
+ chmod 600 "$TOKEN_FILE"
145
+ printf '%s' "$TOKEN" > "$TOKEN_FILE"
146
+
147
+ # Emit only non-secret fields. jq handles escaping. `env` reflects the token's
148
+ # detected environment (prod when undetectable), matching the chosen baseUrl.
149
+ ENV_LABEL="${DETECTED_ENV:-prod}"
150
+ jq -cn --arg baseUrl "$BASE_URL" --arg tokenFile "$TOKEN_FILE" --arg source "$SOURCE" --arg env "$ENV_LABEL" \
151
+ '{baseUrl:$baseUrl, tokenFile:$tokenFile, source:$source, env:$env}'