@salesforce/afv-skills 1.28.0 → 1.29.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/dx-code-analyzer-configure/SKILL.md +31 -13
- package/skills/dx-code-analyzer-custom-rule-create/SKILL.md +484 -0
- package/skills/dx-code-analyzer-custom-rule-create/assets/pmd-ruleset-template.xml +31 -0
- package/skills/dx-code-analyzer-custom-rule-create/examples/metadata-xml-example-fields-api.md +87 -0
- package/skills/dx-code-analyzer-custom-rule-create/examples/metadata-xml-example-flows.md +105 -0
- package/skills/dx-code-analyzer-custom-rule-create/examples/metadata-xml-example-permissions.md +95 -0
- package/skills/dx-code-analyzer-custom-rule-create/examples/metadata-xml-examples.md +84 -0
- package/skills/dx-code-analyzer-custom-rule-create/examples/regex-examples.md +127 -0
- package/skills/dx-code-analyzer-custom-rule-create/examples/xpath-examples.md +227 -0
- package/skills/dx-code-analyzer-custom-rule-create/references/advanced-pmd-patterns.md +288 -0
- package/skills/dx-code-analyzer-custom-rule-create/references/apex-ast-reference.md +127 -0
- package/skills/dx-code-analyzer-custom-rule-create/references/eslint-custom-plugins.md +247 -0
- package/skills/dx-code-analyzer-custom-rule-create/references/eslint-rules-discovery.md +188 -0
- package/skills/dx-code-analyzer-custom-rule-create/references/eslint-tier2-configurable.md +114 -0
- package/skills/dx-code-analyzer-custom-rule-create/references/eslint-tier3-custom-plugins.md +113 -0
- package/skills/dx-code-analyzer-custom-rule-create/references/metadata-xml-rules.md +285 -0
- package/skills/dx-code-analyzer-custom-rule-create/references/regex-rule-schema.md +174 -0
- package/skills/dx-code-analyzer-custom-rule-create/references/troubleshooting.md +141 -0
- package/skills/dx-code-analyzer-custom-rule-create/references/xpath-patterns-governor-limits.md +83 -0
- package/skills/dx-code-analyzer-custom-rule-create/references/xpath-patterns-method-calls.md +108 -0
- package/skills/dx-code-analyzer-custom-rule-create/references/xpath-patterns-security.md +45 -0
- package/skills/dx-code-analyzer-custom-rule-create/references/xpath-patterns-structure.md +127 -0
- package/skills/dx-code-analyzer-custom-rule-create/references/xpath-patterns.md +131 -0
- package/skills/dx-code-analyzer-custom-rule-create/scripts/create-pmd-rule.js +209 -0
- package/skills/dx-code-analyzer-custom-rule-create/scripts/create-regex-rule.js +220 -0
- package/skills/dx-code-analyzer-run/SKILL.md +41 -8
- package/skills/mobile-platform-native-capabilities-integrate/SKILL.md +3 -3
- package/skills/platform-custom-field-generate/SKILL.md +86 -126
- package/skills/platform-custom-field-generate/references/advanced-picklists.md +590 -0
- package/skills/platform-value-set-generate/SKILL.md +305 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Creates a PMD XPath custom rule (XML ruleset file + config reference)
|
|
3
|
+
// Usage: node create-pmd-rule.js --name <name> --xpath <expression> --message <msg> [options]
|
|
4
|
+
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
|
|
8
|
+
function printUsage() {
|
|
9
|
+
console.error(`Usage: node create-pmd-rule.js --name <name> --xpath <expression> --message <msg> [options]
|
|
10
|
+
|
|
11
|
+
Required:
|
|
12
|
+
--name <name> Rule name (PascalCase, no spaces)
|
|
13
|
+
--xpath <expression> XPath expression to match violations
|
|
14
|
+
--message <msg> Violation message shown to users
|
|
15
|
+
|
|
16
|
+
Optional:
|
|
17
|
+
--description <desc> Detailed rule description (default: same as message)
|
|
18
|
+
--language <lang> PMD language (default: apex)
|
|
19
|
+
--priority <1-5> PMD priority (default: 3)
|
|
20
|
+
--example <code> Example violating code snippet
|
|
21
|
+
--config-file <path> Path to code-analyzer.yml (default: ./code-analyzer.yml)
|
|
22
|
+
--ruleset-dir <dir> Directory for ruleset XML (default: ./custom-rules)
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
node create-pmd-rule.js --name NoSystemDebug --xpath "//MethodCallExpression[@FullMethodName='System.debug']" --message "System.debug not allowed" --priority 3
|
|
26
|
+
node create-pmd-rule.js --name SoqlInLoop --xpath "//ForEachStatement//SoqlExpression" --message "SOQL inside loop" --priority 2`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Parse arguments
|
|
31
|
+
const args = process.argv.slice(2);
|
|
32
|
+
if (args.length < 1 || args[0] === "--help" || args[0] === "-h") {
|
|
33
|
+
printUsage();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const options = {
|
|
37
|
+
name: null,
|
|
38
|
+
xpath: null,
|
|
39
|
+
message: null,
|
|
40
|
+
description: null,
|
|
41
|
+
language: "apex",
|
|
42
|
+
priority: 3,
|
|
43
|
+
example: null,
|
|
44
|
+
configFile: "./code-analyzer.yml",
|
|
45
|
+
rulesetDir: "./custom-rules",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < args.length; i++) {
|
|
49
|
+
switch (args[i]) {
|
|
50
|
+
case "--name": options.name = args[++i]; break;
|
|
51
|
+
case "--xpath": options.xpath = args[++i]; break;
|
|
52
|
+
case "--message": options.message = args[++i]; break;
|
|
53
|
+
case "--description": options.description = args[++i]; break;
|
|
54
|
+
case "--language": options.language = args[++i]; break;
|
|
55
|
+
case "--priority": options.priority = parseInt(args[++i], 10); break;
|
|
56
|
+
case "--example": options.example = args[++i]; break;
|
|
57
|
+
case "--config-file": options.configFile = args[++i]; break;
|
|
58
|
+
case "--ruleset-dir": options.rulesetDir = args[++i]; break;
|
|
59
|
+
default:
|
|
60
|
+
console.error(`Unknown option: ${args[i]}`);
|
|
61
|
+
printUsage();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Validate required fields
|
|
66
|
+
if (!options.name) { console.error("Error: --name is required"); process.exit(1); }
|
|
67
|
+
if (!options.xpath) { console.error("Error: --xpath is required"); process.exit(1); }
|
|
68
|
+
if (!options.message) { console.error("Error: --message is required"); process.exit(1); }
|
|
69
|
+
|
|
70
|
+
// Validate rule name
|
|
71
|
+
const RULE_NAME_PATTERN = /^[A-Za-z@][A-Za-z_0-9@\-/]*$/;
|
|
72
|
+
if (!RULE_NAME_PATTERN.test(options.name)) {
|
|
73
|
+
console.error(`Error: Invalid rule name "${options.name}". Must match: ${RULE_NAME_PATTERN}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Validate priority
|
|
78
|
+
if (options.priority < 1 || options.priority > 5) {
|
|
79
|
+
console.error("Error: Priority must be 1-5");
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Validate language
|
|
84
|
+
const VALID_LANGUAGES = ["apex", "visualforce", "html", "xml", "javascript"];
|
|
85
|
+
if (!VALID_LANGUAGES.includes(options.language.toLowerCase())) {
|
|
86
|
+
console.error(`Error: Invalid language "${options.language}". Supported: ${VALID_LANGUAGES.join(", ")}`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Set defaults
|
|
91
|
+
if (!options.description) {
|
|
92
|
+
options.description = options.message;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Generate the PMD ruleset XML
|
|
96
|
+
function buildRulesetXml() {
|
|
97
|
+
const escXpath = options.xpath.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
98
|
+
const escMessage = options.message.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
99
|
+
const escDescription = options.description.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
100
|
+
|
|
101
|
+
let xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
102
|
+
<ruleset name="${options.name}CustomRules"
|
|
103
|
+
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
|
|
104
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
105
|
+
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
|
|
106
|
+
|
|
107
|
+
<description>Custom rules for ${options.name}</description>
|
|
108
|
+
|
|
109
|
+
<rule name="${options.name}"
|
|
110
|
+
language="${options.language}"
|
|
111
|
+
message="${escMessage}"
|
|
112
|
+
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
|
|
113
|
+
|
|
114
|
+
<description>${escDescription}</description>
|
|
115
|
+
<priority>${options.priority}</priority>
|
|
116
|
+
|
|
117
|
+
<properties>
|
|
118
|
+
<property name="xpath">
|
|
119
|
+
<value><![CDATA[
|
|
120
|
+
${options.xpath}
|
|
121
|
+
]]></value>
|
|
122
|
+
</property>
|
|
123
|
+
</properties>`;
|
|
124
|
+
|
|
125
|
+
if (options.example) {
|
|
126
|
+
xml += `
|
|
127
|
+
|
|
128
|
+
<example>
|
|
129
|
+
<![CDATA[
|
|
130
|
+
${options.example}
|
|
131
|
+
]]>
|
|
132
|
+
</example>`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
xml += `
|
|
136
|
+
</rule>
|
|
137
|
+
</ruleset>
|
|
138
|
+
`;
|
|
139
|
+
|
|
140
|
+
return xml;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Create ruleset directory if needed
|
|
144
|
+
const rulesetDir = path.resolve(options.rulesetDir);
|
|
145
|
+
if (!fs.existsSync(rulesetDir)) {
|
|
146
|
+
fs.mkdirSync(rulesetDir, { recursive: true });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Write ruleset XML
|
|
150
|
+
const rulesetFileName = `${options.name}-pmd-ruleset.xml`;
|
|
151
|
+
const rulesetPath = path.join(rulesetDir, rulesetFileName);
|
|
152
|
+
|
|
153
|
+
if (fs.existsSync(rulesetPath)) {
|
|
154
|
+
console.error(`Error: Ruleset file already exists: ${rulesetPath}`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const rulesetXml = buildRulesetXml();
|
|
159
|
+
fs.writeFileSync(rulesetPath, rulesetXml, "utf8");
|
|
160
|
+
|
|
161
|
+
// Update code-analyzer.yml to reference the ruleset
|
|
162
|
+
const configPath = path.resolve(options.configFile);
|
|
163
|
+
const rulesetRelativePath = path.relative(path.dirname(configPath), rulesetPath);
|
|
164
|
+
let configContent = "";
|
|
165
|
+
|
|
166
|
+
if (fs.existsSync(configPath)) {
|
|
167
|
+
configContent = fs.readFileSync(configPath, "utf8");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Check if this ruleset path is already referenced in config (deduplication)
|
|
171
|
+
const alreadyReferenced = configContent.includes(rulesetRelativePath);
|
|
172
|
+
|
|
173
|
+
if (!configContent) {
|
|
174
|
+
// Create new config
|
|
175
|
+
configContent = `engines:\n pmd:\n custom_rulesets:\n - "${rulesetRelativePath}"\n`;
|
|
176
|
+
} else if (alreadyReferenced) {
|
|
177
|
+
// Ruleset path already in config — skip adding duplicate entry
|
|
178
|
+
// (This happens when the XML was deleted and recreated during iteration)
|
|
179
|
+
} else if (configContent.includes("custom_rulesets:") && configContent.includes("pmd:")) {
|
|
180
|
+
// Add to existing custom_rulesets
|
|
181
|
+
const insertPoint = configContent.indexOf("custom_rulesets:");
|
|
182
|
+
const afterLine = configContent.indexOf("\n", insertPoint) + 1;
|
|
183
|
+
configContent = configContent.slice(0, afterLine) + ` - "${rulesetRelativePath}"\n` + configContent.slice(afterLine);
|
|
184
|
+
} else if (configContent.includes("pmd:")) {
|
|
185
|
+
// Add custom_rulesets under pmd
|
|
186
|
+
const insertPoint = configContent.indexOf("pmd:");
|
|
187
|
+
const afterLine = configContent.indexOf("\n", insertPoint) + 1;
|
|
188
|
+
configContent = configContent.slice(0, afterLine) + ` custom_rulesets:\n - "${rulesetRelativePath}"\n` + configContent.slice(afterLine);
|
|
189
|
+
} else if (configContent.includes("engines:")) {
|
|
190
|
+
// Add pmd section under engines
|
|
191
|
+
const insertPoint = configContent.indexOf("engines:");
|
|
192
|
+
const afterLine = configContent.indexOf("\n", insertPoint) + 1;
|
|
193
|
+
configContent = configContent.slice(0, afterLine) + ` pmd:\n custom_rulesets:\n - "${rulesetRelativePath}"\n` + configContent.slice(afterLine);
|
|
194
|
+
} else {
|
|
195
|
+
// Append engines section
|
|
196
|
+
configContent += `\nengines:\n pmd:\n custom_rulesets:\n - "${rulesetRelativePath}"\n`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
fs.writeFileSync(configPath, configContent, "utf8");
|
|
200
|
+
|
|
201
|
+
console.log(JSON.stringify({
|
|
202
|
+
status: "success",
|
|
203
|
+
ruleName: options.name,
|
|
204
|
+
engine: "pmd",
|
|
205
|
+
language: options.language,
|
|
206
|
+
rulesetFile: rulesetPath,
|
|
207
|
+
configFile: configPath,
|
|
208
|
+
message: `Rule "${options.name}" created. Validate with: sf code-analyzer rules --rule-selector pmd:${options.name}`
|
|
209
|
+
}));
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Creates a regex custom rule in code-analyzer.yml
|
|
3
|
+
// Usage: node create-regex-rule.js --name <name> --regex <pattern> --description <desc> [options]
|
|
4
|
+
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
|
|
8
|
+
function printUsage() {
|
|
9
|
+
console.error(`Usage: node create-regex-rule.js --name <name> --regex <pattern> --description <desc> [options]
|
|
10
|
+
|
|
11
|
+
Required:
|
|
12
|
+
--name <name> Rule name (PascalCase, no spaces)
|
|
13
|
+
--regex <pattern> Regex in /pattern/flags format
|
|
14
|
+
--description <desc> What the rule checks
|
|
15
|
+
|
|
16
|
+
Optional:
|
|
17
|
+
--violation-message <msg> Message shown on violation
|
|
18
|
+
--severity <1-5> Severity level (default: 3)
|
|
19
|
+
--tags <tag1,tag2> Comma-separated tags (default: Recommended,Custom)
|
|
20
|
+
--file-extensions <exts> Comma-separated extensions (e.g., .cls,.trigger)
|
|
21
|
+
--regex-ignore <pattern> Negative pattern to exclude matches
|
|
22
|
+
--config-file <path> Path to code-analyzer.yml (default: ./code-analyzer.yml)
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
node create-regex-rule.js --name NoHardcodedIds --regex "/[0-9a-zA-Z]{18}/g" --description "Detects hardcoded IDs" --severity 2 --file-extensions ".cls,.trigger"
|
|
26
|
+
node create-regex-rule.js --name NoTodos --regex "/TODO|FIXME/gi" --description "Flags TODO comments" --severity 4`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Parse arguments
|
|
31
|
+
const args = process.argv.slice(2);
|
|
32
|
+
if (args.length < 1 || args[0] === "--help" || args[0] === "-h") {
|
|
33
|
+
printUsage();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const options = {
|
|
37
|
+
name: null,
|
|
38
|
+
regex: null,
|
|
39
|
+
description: null,
|
|
40
|
+
violationMessage: null,
|
|
41
|
+
severity: 3,
|
|
42
|
+
tags: ["Recommended", "Custom"],
|
|
43
|
+
fileExtensions: null,
|
|
44
|
+
regexIgnore: null,
|
|
45
|
+
configFile: "./code-analyzer.yml",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < args.length; i++) {
|
|
49
|
+
switch (args[i]) {
|
|
50
|
+
case "--name": options.name = args[++i]; break;
|
|
51
|
+
case "--regex": options.regex = args[++i]; break;
|
|
52
|
+
case "--description": options.description = args[++i]; break;
|
|
53
|
+
case "--violation-message": options.violationMessage = args[++i]; break;
|
|
54
|
+
case "--severity": options.severity = parseInt(args[++i], 10); break;
|
|
55
|
+
case "--tags": options.tags = args[++i].split(",").map(t => t.trim()); break;
|
|
56
|
+
case "--file-extensions": options.fileExtensions = args[++i].split(",").map(e => e.trim()); break;
|
|
57
|
+
case "--regex-ignore": options.regexIgnore = args[++i]; break;
|
|
58
|
+
case "--config-file": options.configFile = args[++i]; break;
|
|
59
|
+
default:
|
|
60
|
+
console.error(`Unknown option: ${args[i]}`);
|
|
61
|
+
printUsage();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Validate required fields
|
|
66
|
+
if (!options.name) { console.error("Error: --name is required"); process.exit(1); }
|
|
67
|
+
if (!options.regex) { console.error("Error: --regex is required"); process.exit(1); }
|
|
68
|
+
if (!options.description) { console.error("Error: --description is required"); process.exit(1); }
|
|
69
|
+
|
|
70
|
+
// Validate rule name
|
|
71
|
+
const RULE_NAME_PATTERN = /^[A-Za-z@][A-Za-z_0-9@\-/]*$/;
|
|
72
|
+
if (!RULE_NAME_PATTERN.test(options.name)) {
|
|
73
|
+
console.error(`Error: Invalid rule name "${options.name}". Must match: ${RULE_NAME_PATTERN}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Validate regex format — must be /pattern/flags with no surrounding whitespace
|
|
78
|
+
// and the flags portion must contain only valid JavaScript regex flag characters.
|
|
79
|
+
options.regex = options.regex.trim();
|
|
80
|
+
if (!options.regex.startsWith("/") || options.regex.lastIndexOf("/") <= 0) {
|
|
81
|
+
console.error(`Error: Regex must be in /pattern/flags format. Got: "${options.regex}"`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
const lastSlash = options.regex.lastIndexOf("/");
|
|
85
|
+
const flags = options.regex.slice(lastSlash + 1);
|
|
86
|
+
if (!flags) {
|
|
87
|
+
console.error(`Error: Regex must include flags after the closing /. Use /pattern/g at minimum. Got: "${options.regex}"`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
// Strict flags validation — only valid JS regex flag chars allowed, no spaces, no junk.
|
|
91
|
+
if (!/^[gimsuy]+$/.test(flags)) {
|
|
92
|
+
console.error(`Error: Invalid regex flags "${flags}". Allowed: g, i, m, s, u, y (no spaces, no other characters). Got: "${options.regex}"`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
// Code Analyzer regex rules require the global flag.
|
|
96
|
+
if (!flags.includes("g")) {
|
|
97
|
+
console.error(`Error: Regex must include the global flag 'g'. Got flags: "${flags}"`);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Validate severity
|
|
102
|
+
if (options.severity < 1 || options.severity > 5) {
|
|
103
|
+
console.error("Error: Severity must be 1-5");
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Validate file extensions
|
|
108
|
+
if (options.fileExtensions) {
|
|
109
|
+
for (const ext of options.fileExtensions) {
|
|
110
|
+
if (!ext.startsWith(".")) {
|
|
111
|
+
console.error(`Error: File extension must start with dot: "${ext}"`);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Safely quote a string for YAML output.
|
|
118
|
+
// CRITICAL: regex patterns contain backslashes (`\.`, `\d`, `\\`, etc.).
|
|
119
|
+
// Double-quoted YAML treats `\` as an escape introducer — `\.` is an unknown
|
|
120
|
+
// escape and YAML rejects the file with "unknown escape sequence". Single-quoted
|
|
121
|
+
// YAML treats backslash as literal, which is exactly what regex needs.
|
|
122
|
+
// Strategy:
|
|
123
|
+
// - If value contains a backslash → ALWAYS use single quotes (escape ' as '')
|
|
124
|
+
// - Else if value has no quotes → double quotes (simplest, most readable)
|
|
125
|
+
// - Else → single quotes (escape ' as '')
|
|
126
|
+
function yamlQuote(value) {
|
|
127
|
+
const hasBackslash = value.includes("\\");
|
|
128
|
+
const hasSingle = value.includes("'");
|
|
129
|
+
const hasDouble = value.includes('"');
|
|
130
|
+
|
|
131
|
+
if (!hasBackslash && !hasSingle && !hasDouble) {
|
|
132
|
+
return `"${value}"`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Single-quoted YAML: only ' needs escaping (as ''). Backslashes pass through.
|
|
136
|
+
const escaped = value.replace(/'/g, "''");
|
|
137
|
+
return `'${escaped}'`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Build the rule YAML block
|
|
141
|
+
function buildRuleYaml() {
|
|
142
|
+
const indent = " ";
|
|
143
|
+
const lines = [];
|
|
144
|
+
lines.push(` ${options.name}:`);
|
|
145
|
+
lines.push(`${indent} regex: ${yamlQuote(options.regex)}`);
|
|
146
|
+
lines.push(`${indent} description: ${yamlQuote(options.description)}`);
|
|
147
|
+
|
|
148
|
+
if (options.violationMessage) {
|
|
149
|
+
lines.push(`${indent} violation_message: ${yamlQuote(options.violationMessage)}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
lines.push(`${indent} severity: ${options.severity}`);
|
|
153
|
+
|
|
154
|
+
if (options.tags && options.tags.length > 0) {
|
|
155
|
+
lines.push(`${indent} tags:`);
|
|
156
|
+
options.tags.forEach(tag => lines.push(`${indent} - "${tag}"`));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (options.fileExtensions && options.fileExtensions.length > 0) {
|
|
160
|
+
lines.push(`${indent} file_extensions:`);
|
|
161
|
+
options.fileExtensions.forEach(ext => lines.push(`${indent} - "${ext}"`));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (options.regexIgnore) {
|
|
165
|
+
lines.push(`${indent} regex_ignore: ${yamlQuote(options.regexIgnore)}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return lines.join("\n");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Read or create config file
|
|
172
|
+
const configPath = path.resolve(options.configFile);
|
|
173
|
+
let configContent = "";
|
|
174
|
+
|
|
175
|
+
if (fs.existsSync(configPath)) {
|
|
176
|
+
configContent = fs.readFileSync(configPath, "utf8");
|
|
177
|
+
|
|
178
|
+
// Check if rule already exists
|
|
179
|
+
if (configContent.includes(`${options.name}:`)) {
|
|
180
|
+
console.error(`Error: Rule "${options.name}" already exists in ${configPath}`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const ruleYaml = buildRuleYaml();
|
|
186
|
+
|
|
187
|
+
// Upsert into config
|
|
188
|
+
if (!configContent) {
|
|
189
|
+
// Create new file
|
|
190
|
+
configContent = `engines:\n regex:\n custom_rules:\n${ruleYaml}\n`;
|
|
191
|
+
} else if (configContent.includes("custom_rules:") && configContent.includes("regex:")) {
|
|
192
|
+
// Add to existing custom_rules section
|
|
193
|
+
const insertPoint = configContent.indexOf("custom_rules:");
|
|
194
|
+
const afterCustomRules = configContent.indexOf("\n", insertPoint) + 1;
|
|
195
|
+
configContent = configContent.slice(0, afterCustomRules) + ruleYaml + "\n" + configContent.slice(afterCustomRules);
|
|
196
|
+
} else if (configContent.includes("regex:")) {
|
|
197
|
+
// Add custom_rules section under regex
|
|
198
|
+
const insertPoint = configContent.indexOf("regex:");
|
|
199
|
+
const afterRegex = configContent.indexOf("\n", insertPoint) + 1;
|
|
200
|
+
configContent = configContent.slice(0, afterRegex) + " custom_rules:\n" + ruleYaml + "\n" + configContent.slice(afterRegex);
|
|
201
|
+
} else if (configContent.includes("engines:")) {
|
|
202
|
+
// Add regex section under engines
|
|
203
|
+
const insertPoint = configContent.indexOf("engines:");
|
|
204
|
+
const afterEngines = configContent.indexOf("\n", insertPoint) + 1;
|
|
205
|
+
configContent = configContent.slice(0, afterEngines) + " regex:\n custom_rules:\n" + ruleYaml + "\n" + configContent.slice(afterEngines);
|
|
206
|
+
} else {
|
|
207
|
+
// Append engines section
|
|
208
|
+
configContent += "\nengines:\n regex:\n custom_rules:\n" + ruleYaml + "\n";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Write config
|
|
212
|
+
fs.writeFileSync(configPath, configContent, "utf8");
|
|
213
|
+
|
|
214
|
+
console.log(JSON.stringify({
|
|
215
|
+
status: "success",
|
|
216
|
+
ruleName: options.name,
|
|
217
|
+
engine: "regex",
|
|
218
|
+
configFile: configPath,
|
|
219
|
+
message: `Rule "${options.name}" created. Validate with: sf code-analyzer rules --rule-selector regex:${options.name}`
|
|
220
|
+
}));
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dx-code-analyzer-run
|
|
3
|
-
description: "Run Salesforce Code Analyzer to scan code for security, performance, best practice, and code style violations. Supports all engines (PMD, ESLint, CPD, RetireJS, Flow, SFGE, ApexGuru), targets (files, folders, git diff), categories, and severities. Also handles post-scan exploration: filtering results by engine/severity/category/file, and explaining what
|
|
4
|
-
allowed-tools: Read, Bash(sf code-analyzer), Bash(node), Bash(git diff), Bash(date), Write, Edit
|
|
3
|
+
description: "Run Salesforce Code Analyzer to scan code for security, performance, best practice, and code style violations. Supports all engines (PMD, ESLint, CPD, RetireJS, Flow, SFGE, ApexGuru), targets (files, folders, git diff), categories, and severities. Also handles post-scan exploration: filtering results by engine/severity/category/file, and explaining what rules mean. TRIGGER when: user says 'scan my code', 'check security issues', 'run PMD/ESLint', 'find duplicates', 'analyze Flows', 'check vulnerable libraries', 'AppExchange review', 'lint my LWC', 'static analysis', 'code quality', 'show security violations', 'what is this rule', 'explain ApexCRUDViolation', 'filter results', or mentions engines/file types (.cls, .trigger, .js, .flow-meta.xml). Use this skill for scanning, exploring results, and listing rules. DO NOT TRIGGER when: user asks only about installation/configuration (use dx-code-analyzer-configure), or wants to create a custom rule (use dx-code-analyzer-custom-rule-create)."
|
|
5
4
|
metadata:
|
|
6
5
|
version: "1.0"
|
|
7
|
-
|
|
6
|
+
relatedSkills:
|
|
7
|
+
- "dx-code-analyzer-configure"
|
|
8
|
+
- "dx-code-analyzer-custom-rule-create"
|
|
9
|
+
cliTools:
|
|
10
|
+
- tool: ["sf"]
|
|
11
|
+
semver: ">=2.0.0"
|
|
12
|
+
- tool: ["node"]
|
|
13
|
+
semver: ">=18.0.0"
|
|
14
|
+
- tool: ["git"]
|
|
15
|
+
semver: ">=2.0.0"
|
|
8
16
|
---
|
|
9
17
|
|
|
10
18
|
# Running Code Analyzer Skill
|
|
@@ -67,11 +75,13 @@ Any aggregation, filter, or rank question ("which file has the most violations?"
|
|
|
67
75
|
|
|
68
76
|
## Overview
|
|
69
77
|
|
|
78
|
+
> **Ecosystem:** This skill is part of a 3-skill Code Analyzer suite — `dx-code-analyzer-run` (scans & results) · `dx-code-analyzer-configure` (setup, config, CI/CD) · `dx-code-analyzer-custom-rule-create` (custom rule authoring).
|
|
79
|
+
|
|
70
80
|
This skill translates natural-language requests ("scan for security issues", "check my changes") into the correct `sf code-analyzer run` command, executes scans across any combination of engines/targets/severities, and presents actionable results. When engine-provided fixes are available, it discovers them, asks for user confirmation, applies them safely, and offers verification. Use it for static analysis, security reviews, AppExchange certification, code-quality checks, and finding duplicates/vulnerabilities in Salesforce projects.
|
|
71
81
|
|
|
72
82
|
**In scope:** running scans, parsing/filtering/ranking results, applying engine auto-fixes, diff-based scans, all output formats (JSON/HTML/SARIF/CSV/XML), describing/listing rules, scan-failure troubleshooting.
|
|
73
83
|
|
|
74
|
-
**Out of scope:** installing/configuring `sf` or the plugin (→ `dx-code-analyzer-configure`), writing custom rules/engines, AI-generated fixes beyond engine-provided ones, deep refactoring, CI/CD setup (→ `dx-code-analyzer-configure`).
|
|
84
|
+
**Out of scope:** installing/configuring `sf` or the plugin (→ `dx-code-analyzer-configure`), writing custom rules/engines (→ `dx-code-analyzer-custom-rule-create`), AI-generated fixes beyond engine-provided ones, deep refactoring, CI/CD setup (→ `dx-code-analyzer-configure`).
|
|
75
85
|
|
|
76
86
|
**Allowed tools:** Bash (`sf code-analyzer`, `node`, `git diff`, `date`), Read, Write, Edit. **Forbidden:** any MCP tool, Agent tool, web tools, other skills, Python, `jq`, inline scripts/heredocs. This skill owns the complete scan-fix-verify-query-explain workflow end-to-end.
|
|
77
87
|
|
|
@@ -203,7 +213,7 @@ Use the **Bash tool only** — never the `run_code_analyzer` MCP tool.
|
|
|
203
213
|
|
|
204
214
|
1. Generate the timestamp via Bash: `date +%Y%m%d-%H%M%S` → e.g. `20260512-143022`.
|
|
205
215
|
2. Tell the user:
|
|
206
|
-
```
|
|
216
|
+
```text
|
|
207
217
|
Starting scan...
|
|
208
218
|
Results: ./code-analyzer-results-20260512-143022.json
|
|
209
219
|
Log: ./code-analyzer-results-20260512-143022.log
|
|
@@ -237,7 +247,7 @@ node "<skill_dir>/scripts/parse-results.js" "./code-analyzer-results-TIMESTAMP.j
|
|
|
237
247
|
|
|
238
248
|
### Presentation template
|
|
239
249
|
|
|
240
|
-
```
|
|
250
|
+
```text
|
|
241
251
|
## Scan Complete
|
|
242
252
|
|
|
243
253
|
**Found X violations** across Y files.
|
|
@@ -296,7 +306,7 @@ node "<skill_dir>/scripts/discover-fixes.js" "./code-analyzer-results-TIMESTAMP.
|
|
|
296
306
|
|
|
297
307
|
### 6.3 Present + ASK (then STOP)
|
|
298
308
|
|
|
299
|
-
```
|
|
309
|
+
```text
|
|
300
310
|
### Engine-Provided Fixes Available
|
|
301
311
|
**X of Y violations** have auto-fixes provided by the analysis engine:
|
|
302
312
|
|
|
@@ -327,7 +337,7 @@ node "<skill_dir>/scripts/summarize-fixes.js" "./code-analyzer-results-TIMESTAMP
|
|
|
327
337
|
|
|
328
338
|
Then present:
|
|
329
339
|
|
|
330
|
-
```
|
|
340
|
+
```text
|
|
331
341
|
### Engine-Provided Fixes Applied Successfully ✓
|
|
332
342
|
**Applied X auto-fixes across Y files.**
|
|
333
343
|
|
|
@@ -439,6 +449,29 @@ node "<skill_dir>/scripts/list-rules.js" "<selector>" [options]
|
|
|
439
449
|
|
|
440
450
|
Filters: `--engine`, `--severity`, `--top` (default 100), `--count-only`. The script pre-validates selector tokens (catches typos like `secruity`) before calling the CLI. Presentation: `<skill_dir>/references/post-scan-workflows.md`.
|
|
441
451
|
|
|
452
|
+
---
|
|
453
|
+
## Cross-Skill Integration
|
|
454
|
+
|
|
455
|
+
This skill is part of a 3-skill Code Analyzer ecosystem. Hand off cleanly rather than attempting work that belongs to another skill.
|
|
456
|
+
|
|
457
|
+
### When THIS skill delegates to `dx-code-analyzer-configure`:
|
|
458
|
+
|
|
459
|
+
- Pre-flight check fails (CLI missing, plugin not installed, engine prereqs broken) → stop, delegate, return here after fix
|
|
460
|
+
- User asks to set up CI/CD, edit `code-analyzer.yml`, change severities, or disable engines → delegate entirely
|
|
461
|
+
|
|
462
|
+
### When THIS skill delegates to `dx-code-analyzer-custom-rule-create`:
|
|
463
|
+
|
|
464
|
+
- User asks to create a new rule, write XPath, write a regex rule, or enforce a pattern not covered by built-in rules → delegate entirely. Do NOT attempt to create rules here.
|
|
465
|
+
|
|
466
|
+
### When other skills hand off HERE:
|
|
467
|
+
|
|
468
|
+
- `dx-code-analyzer-configure` completes setup → proceed with scan (Step 1–5)
|
|
469
|
+
- `dx-code-analyzer-custom-rule-create` finishes creating a rule → proceed with scan targeting the new rule (e.g., `--rule-selector pmd:<RuleName>`) to verify it works
|
|
470
|
+
|
|
471
|
+
### Ownership boundary
|
|
472
|
+
|
|
473
|
+
This skill owns the complete **scan → explore → fix** workflow end-to-end. It does NOT own installation, config file management, or rule authoring.
|
|
474
|
+
|
|
442
475
|
---
|
|
443
476
|
|
|
444
477
|
## Constraints & Gotchas
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mobile-platform-native-capabilities-integrate
|
|
3
|
-
description: "Build a Salesforce LWC that uses native mobile device capabilities — barcode scanner, biometrics, location, NFC, calendar, contacts, document scanner, geofencing, AR space capture, app review, and payments. Use this skill when the user asks for an LWC that scans a barcode, captures a photo of a document, reads location or geofences, prompts for biometrics, reads/writes the device calendar or contacts, taps NFC, takes a payment, prompts for an app review, or scans an AR space. Also triggers on \"lightning/mobileCapabilities\", \"mobile capability\", \"Nimbus\", \"device capability\". Do not use for mobile offline / Komaci priming reviews (use `mobile-platform-offline-validate`) or for picking generic Lightning base components (use
|
|
3
|
+
description: "Build a Salesforce LWC that uses native mobile device capabilities — barcode scanner, biometrics, location, NFC, calendar, contacts, document scanner, geofencing, AR space capture, app review, and payments. Use this skill when the user asks for an LWC that scans a barcode, captures a photo of a document, reads location or geofences, prompts for biometrics, reads/writes the device calendar or contacts, taps NFC, takes a payment, prompts for an app review, or scans an AR space. Also triggers on \"lightning/mobileCapabilities\", \"mobile capability\", \"Nimbus\", \"device capability\". Do not use for mobile offline / Komaci priming reviews (use `mobile-platform-offline-validate`) or for picking generic Lightning base components (use `design-systems-slds-apply`)."
|
|
4
4
|
metadata:
|
|
5
5
|
version: "1.0"
|
|
6
6
|
---
|
|
@@ -32,8 +32,8 @@ Do NOT use this skill for:
|
|
|
32
32
|
|
|
33
33
|
- Mobile-offline review of an LWC (lwc:if, inline GraphQL, Komaci-priming
|
|
34
34
|
violations) — use `mobile-platform-offline-validate`.
|
|
35
|
-
-
|
|
36
|
-
`
|
|
35
|
+
- Choosing or styling generic Lightning Base Components / SLDS blueprints —
|
|
36
|
+
use `design-systems-slds-apply`.
|
|
37
37
|
|
|
38
38
|
## Prerequisites
|
|
39
39
|
|