@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.
- package/package.json +1 -1
- package/skills/automation-sandbox-post-copy-config-generate/SKILL.md +239 -0
- package/skills/automation-sandbox-post-copy-config-generate/assets/config_template.json +21 -0
- package/skills/automation-sandbox-post-copy-config-generate/assets/json_schema.json +90 -0
- package/skills/automation-sandbox-post-copy-config-generate/examples/sample_sop_excerpt.md +31 -0
- package/skills/automation-sandbox-post-copy-config-generate/examples/sample_sop_to_config.json +50 -0
- package/skills/automation-sandbox-post-copy-config-generate/references/configuration_catalog.md +76 -0
- package/skills/automation-sandbox-post-copy-config-generate/references/sop_parsing_patterns.md +157 -0
- package/skills/automation-sandbox-post-copy-config-generate/references/source_format_handling.md +230 -0
- package/skills/dx-apexguru-scan/SKILL.md +403 -0
- package/skills/dx-apexguru-scan/examples/README.md +54 -0
- package/skills/dx-apexguru-scan/examples/sample-decoded-summary.json +176 -0
- package/skills/dx-apexguru-scan/examples/sample-full-no-runtime-response.json +26 -0
- package/skills/dx-apexguru-scan/examples/sample-succeeded-response.json +15 -0
- package/skills/dx-apexguru-scan/references/api-reference.md +81 -0
- package/skills/dx-apexguru-scan/references/authentication.md +134 -0
- package/skills/dx-apexguru-scan/references/error-handling.md +56 -0
- package/skills/dx-apexguru-scan/references/violation-catalog.md +28 -0
- package/skills/dx-apexguru-scan/scripts/build-zip.sh +87 -0
- package/skills/dx-apexguru-scan/scripts/decode-report.js +389 -0
- package/skills/dx-apexguru-scan/scripts/resolve-token.sh +151 -0
- package/skills/dx-apexguru-scan/scripts/run-scan.sh +153 -0
- package/skills/dx-apexguru-scan/scripts/scan.sh +96 -0
- package/skills/dx-apexguru-scan/scripts/validate-token.js +121 -0
- package/skills/dx-devops-pipeline-manage/SKILL.md +263 -0
- package/skills/dx-devops-pipeline-manage/examples/common-workflows.md +177 -0
- package/skills/dx-devops-pipeline-manage/references/cli-commands.md +298 -0
- package/skills/dx-devops-pipeline-manage/references/parsing-patterns.md +134 -0
- package/skills/dx-devops-pipeline-manage/scripts/check-activation-ready.sh +34 -0
- package/skills/dx-devops-pipeline-manage/scripts/validate-org-type.sh +17 -0
- package/skills/dx-devops-pipeline-manage/scripts/verify-operation.sh +82 -0
- package/skills/dx-devops-promote/SKILL.md +214 -0
- package/skills/dx-devops-promote/examples/promotion-workflows.md +212 -0
- package/skills/dx-devops-promote/references/cli-commands.md +303 -0
- package/skills/experience-lwc-base-components-integrate/SKILL.md +176 -0
- package/skills/experience-lwc-base-components-integrate/references/lbc-expert-guidance.md +127 -0
- package/skills/experience-lwc-base-components-integrate/references/lightning-component-index.md +179 -0
- package/skills/experience-lwc-base-components-integrate/references/lightning-components.md +5429 -0
- package/skills/experience-lwc-base-components-integrate/scripts/extract-component-docs.sh +61 -0
- package/skills/experience-lwc-rtl-validate/SKILL.md +149 -0
- package/skills/experience-lwc-rtl-validate/references/rtl-expert.md +892 -0
- package/skills/experience-lwc-rtl-validate/scripts/scan-rtl-css.sh +206 -0
- package/skills/experience-lwc-typescript-migrate/SKILL.md +207 -0
- package/skills/experience-lwc-typescript-migrate/assets/dts-template.ts +15 -0
- package/skills/experience-lwc-typescript-migrate/assets/type-patterns.ts +44 -0
- package/skills/experience-lwc-typescript-migrate/scripts/find-consumers.sh +128 -0
- package/skills/experience-ui-bundle-localize/SKILL.md +323 -0
- package/skills/experience-ui-bundle-localize/references/gotchas.md +249 -0
- package/skills/experience-ui-bundle-localize/references/i18n-setup.md +169 -0
- package/skills/experience-ui-bundle-localize/references/interpolation.md +311 -0
- package/skills/experience-ui-bundle-localize/references/label-xml.md +282 -0
- package/skills/experience-ui-bundle-localize/references/verifying.md +219 -0
- package/skills/experience-ui-bundle-localize/scripts/check-i18n-wired.sh +195 -0
- package/skills/experience-ui-bundle-localize/scripts/check-manifest-registered.sh +100 -0
- package/skills/experience-ui-bundle-localize/scripts/check-org-api-version.sh +40 -0
- package/skills/experience-ui-bundle-localize/scripts/detect-bundle-type.sh +57 -0
- package/skills/platform-custom-lightning-type-generate/SKILL.md +3 -0
- package/skills/platform-custom-lightning-type-generate/assets/primitive-types-and-constraints.md +1 -1
- package/skills/platform-mcp-tool-widget-coordinate/SKILL.md +250 -0
- package/skills/platform-mcp-tool-widget-coordinate/examples/action-name-source-prompt.md +74 -0
- package/skills/platform-mcp-tool-widget-coordinate/examples/apex-invocable-source-prompt.md +90 -0
- package/skills/platform-mcp-tool-widget-coordinate/examples/nested-object-source-prompt.md +191 -0
- package/skills/platform-mcp-tool-widget-coordinate/examples/pasted-tool-output-prompt.md +85 -0
- package/skills/platform-mcp-tool-widget-coordinate/references/build-plan-format.md +74 -0
- package/skills/platform-mcp-tool-widget-coordinate/references/mcp-tool-output-discovery.md +184 -0
- package/skills/platform-mcp-tool-widget-coordinate/references/two-clt-modeling.md +128 -0
- 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}'
|