sfdx-gatekeeper 1.0.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/README.md ADDED
@@ -0,0 +1,183 @@
1
+ # SFDX Gatekeeper
2
+
3
+ A lightweight pre-commit validation framework for Salesforce projects.
4
+
5
+ **Author:** Milanjeet Singh
6
+ **Created:** March 2026
7
+
8
+ [![npm version](https://badge.fury.io/js/sfdx-gatekeeper.svg)](https://www.npmjs.com/package/sfdx-gatekeeper)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
+
11
+ ## Features
12
+
13
+ - **Easy Configuration** - Simple YAML config file
14
+ - **Extensible** - Add your own validations
15
+ - **Beautiful Output** - Formatted, colorful terminal output
16
+ - **Minimal Dependencies** - Only `js-yaml` for config parsing
17
+ - **Salesforce Focused** - Built specifically for SFDX projects
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install sfdx-gatekeeper --save-dev
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ### 1. Initialize Configuration
28
+
29
+ ```bash
30
+ npx sfdx-gatekeeper init
31
+ ```
32
+
33
+ This creates `sfdx-gatekeeper.yml` in your project root.
34
+
35
+ ### 2. Install Git Hooks
36
+
37
+ ```bash
38
+ npx sfdx-gatekeeper install
39
+ ```
40
+
41
+ ### 3. Done!
42
+
43
+ Your commits will now be validated automatically.
44
+
45
+ ## Sample Output
46
+
47
+ ```
48
+ ┌──────────────────────────────────────────────────┐
49
+ │ SFDX Gatekeeper Validations │
50
+ ├──────────────────────────────────────────────────┤
51
+ │ Commit Message Standard ✔ [Pass] │
52
+ │ Component Prefix (CODVR_) ✔ [Pass] │
53
+ │ Branch Naming Convention ✔ [Pass] │
54
+ ├──────────────────────────────────────────────────┤
55
+ │ Result: 3/3 checks passed │
56
+ └──────────────────────────────────────────────────┘
57
+ ```
58
+
59
+ ## Commands
60
+
61
+ | Command | Description |
62
+ |---------|-------------|
63
+ | `npx sfdx-gatekeeper init` | Create config file |
64
+ | `npx sfdx-gatekeeper install` | Install git hooks |
65
+ | `npx sfdx-gatekeeper uninstall` | Remove git hooks |
66
+ | `npx sfdx-gatekeeper run <hook>` | Run validations manually |
67
+ | `npx sfdx-gatekeeper list` | List available validations |
68
+ | `npx sfdx-gatekeeper test` | Test output formatting |
69
+
70
+ ## Configuration
71
+
72
+ Edit `sfdx-gatekeeper.yml` to customize validations:
73
+
74
+ ```yaml
75
+ settings:
76
+ name: "SFDX Gatekeeper Validations"
77
+ boxWidth: 50
78
+
79
+ validations:
80
+ enabled:
81
+ - commit-message
82
+ - component-prefix
83
+ - branch-naming
84
+
85
+ commit-message:
86
+ prefix: "Commit for"
87
+ issuePattern: "[IS]-[0-9]+"
88
+ blockedPhrases:
89
+ - "initial commit"
90
+ - "dummy"
91
+ - "wip"
92
+ requireDescription: true
93
+
94
+ component-prefix:
95
+ prefix: "CODVR_"
96
+ checkTypes:
97
+ - lwc
98
+ - aura
99
+ - classes
100
+
101
+ branch-naming:
102
+ pattern: "^(feature|bugfix|hotfix|release)/"
103
+ exempt:
104
+ - main
105
+ - master
106
+ - develop
107
+ ```
108
+
109
+ ## Built-in Validations
110
+
111
+ | Validation | Description |
112
+ |------------|-------------|
113
+ | `commit-message` | Validates commit message format |
114
+ | `component-prefix` | Checks SF components have required prefix |
115
+ | `branch-naming` | Enforces branch naming convention |
116
+ | `package-xml` | Checks package.xml is in sync |
117
+
118
+ ## Adding Custom Validations
119
+
120
+ Create a new file in `node_modules/sfdx-gatekeeper/validations/` or your project's validations folder:
121
+
122
+ ```javascript
123
+ // validations/my-custom-check.js
124
+
125
+ class MyCustomValidation {
126
+ constructor(validator) {
127
+ this.validator = validator;
128
+ this.displayName = 'My Custom Check';
129
+ }
130
+
131
+ async run() {
132
+ // Your validation logic here
133
+
134
+ if (someCondition) {
135
+ return { status: 'pass', details: '' };
136
+ }
137
+
138
+ return {
139
+ status: 'fail',
140
+ details: 'Reason for failure'
141
+ };
142
+ }
143
+ }
144
+
145
+ module.exports = MyCustomValidation;
146
+ ```
147
+
148
+ Enable it in `sfdx-gatekeeper.yml`:
149
+
150
+ ```yaml
151
+ validations:
152
+ enabled:
153
+ - commit-message
154
+ - my-custom-check
155
+ ```
156
+
157
+ ## Commit Message Format
158
+
159
+ Valid commit messages:
160
+
161
+ ```
162
+ Commit for I-12345 - Fixed login issue
163
+ Commit for S-67890 - Added new feature
164
+ Commit for codvr_myComponent - Updated styles
165
+ Commit for Apex CODVR_Controller - Refactored query
166
+ ```
167
+
168
+ ## Requirements
169
+
170
+ - Node.js >= 14.0.0
171
+ - Git
172
+
173
+ ## Contributing
174
+
175
+ Contributions are welcome! Please feel free to submit a Pull Request.
176
+
177
+ ## Author
178
+
179
+ **Milanjeet Singh**
180
+
181
+ ## License
182
+
183
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * =============================================================================
4
+ * SFDX Gatekeeper - CLI Entry Point
5
+ *
6
+ * Author: Milanjeet Singh
7
+ * Created: March 2026
8
+ * =============================================================================
9
+ */
10
+
11
+ const { SFDXGatekeeper } = require('../src/index.js');
12
+
13
+ const args = process.argv.slice(2);
14
+ const command = args[0] || 'help';
15
+
16
+ const validator = new SFDXGatekeeper();
17
+
18
+ async function main() {
19
+ try {
20
+ switch (command) {
21
+ case 'init':
22
+ await validator.init();
23
+ break;
24
+
25
+ case 'install':
26
+ await validator.installHooks();
27
+ break;
28
+
29
+ case 'uninstall':
30
+ await validator.uninstallHooks();
31
+ break;
32
+
33
+ case 'run':
34
+ const hook = args[1] || 'pre-commit';
35
+ const hookArg = args[2];
36
+ const exitCode = await validator.run(hook, hookArg);
37
+ process.exit(exitCode);
38
+ break;
39
+
40
+ case 'list':
41
+ validator.listValidations();
42
+ break;
43
+
44
+ case 'test':
45
+ validator.testOutput();
46
+ break;
47
+
48
+ case 'help':
49
+ case '--help':
50
+ case '-h':
51
+ printHelp();
52
+ break;
53
+
54
+ case 'version':
55
+ case '--version':
56
+ case '-v':
57
+ const pkg = require('../package.json');
58
+ console.log(`mtx-validator v${pkg.version}`);
59
+ break;
60
+
61
+ default:
62
+ console.error(`Unknown command: ${command}`);
63
+ printHelp();
64
+ process.exit(1);
65
+ }
66
+ } catch (error) {
67
+ console.error('Error:', error.message);
68
+ process.exit(1);
69
+ }
70
+ }
71
+
72
+ function printHelp() {
73
+ console.log(`
74
+ SFDX Gatekeeper - Pre-commit validation framework for Salesforce
75
+ Author: Milanjeet Singh
76
+
77
+ Usage: sfdx-gatekeeper <command> [options]
78
+
79
+ Commands:
80
+ init Create sfdx-gatekeeper.yml config in current project
81
+ install Install git hooks in current project
82
+ uninstall Remove git hooks from current project
83
+ run <hook> Run validations for a specific hook
84
+ list List available validations
85
+ test Test validator output (dry run)
86
+ version Show version number
87
+ help Show this help message
88
+
89
+ Hooks:
90
+ pre-commit Run before commit is created
91
+ commit-msg <file> Validate commit message
92
+ pre-push Run before push
93
+
94
+ Examples:
95
+ npx sfdx-gatekeeper init
96
+ npx sfdx-gatekeeper install
97
+ npx sfdx-gatekeeper run pre-commit
98
+ npx sfdx-gatekeeper run commit-msg .git/COMMIT_EDITMSG
99
+ `);
100
+ }
101
+
102
+ main();
package/lib/output.js ADDED
@@ -0,0 +1,190 @@
1
+ /**
2
+ * =============================================================================
3
+ * SFDX Gatekeeper - Output Formatting Library
4
+ *
5
+ * Author: Milanjeet Singh
6
+ * Created: March 2026
7
+ * =============================================================================
8
+ */
9
+
10
+ class Output {
11
+ constructor(boxWidth = 50) {
12
+ this.boxWidth = boxWidth;
13
+ this.supportsColor = process.stdout.isTTY;
14
+
15
+ // Colors
16
+ this.colors = {
17
+ red: this.supportsColor ? '\x1b[31m' : '',
18
+ green: this.supportsColor ? '\x1b[32m' : '',
19
+ yellow: this.supportsColor ? '\x1b[33m' : '',
20
+ blue: this.supportsColor ? '\x1b[34m' : '',
21
+ cyan: this.supportsColor ? '\x1b[36m' : '',
22
+ bold: this.supportsColor ? '\x1b[1m' : '',
23
+ dim: this.supportsColor ? '\x1b[2m' : '',
24
+ reset: this.supportsColor ? '\x1b[0m' : ''
25
+ };
26
+
27
+ // Symbols
28
+ this.symbols = {
29
+ pass: '✔',
30
+ fail: '✘',
31
+ warn: '⚠',
32
+ info: 'ℹ',
33
+ arrow: '→'
34
+ };
35
+
36
+ // Box drawing
37
+ this.box = {
38
+ tl: '┌', tr: '┐', bl: '└', br: '┘',
39
+ h: '─', v: '│', ml: '├', mr: '┤'
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Print header with title
45
+ */
46
+ printHeader(title) {
47
+ const { cyan, bold, reset } = this.colors;
48
+ const { tl, tr, h, v, ml, mr } = this.box;
49
+ const width = this.boxWidth;
50
+
51
+ const padding = Math.floor((width - title.length) / 2);
52
+ const paddingStr = ' '.repeat(padding);
53
+ const extraSpace = (width - title.length) % 2 === 1 ? ' ' : '';
54
+
55
+ console.log('');
56
+ console.log(`${cyan}${tl}${h.repeat(width)}${tr}${reset}`);
57
+ console.log(`${cyan}${v}${reset}${bold}${paddingStr}${title}${paddingStr}${extraSpace}${reset}${cyan}${v}${reset}`);
58
+ console.log(`${cyan}${ml}${h.repeat(width)}${mr}${reset}`);
59
+ }
60
+
61
+ /**
62
+ * Print footer
63
+ */
64
+ printFooter() {
65
+ const { cyan, reset } = this.colors;
66
+ const { bl, br, h } = this.box;
67
+
68
+ console.log(`${cyan}${bl}${h.repeat(this.boxWidth)}${br}${reset}`);
69
+ console.log('');
70
+ }
71
+
72
+ /**
73
+ * Print divider
74
+ */
75
+ printDivider() {
76
+ const { cyan, reset } = this.colors;
77
+ const { ml, mr, h } = this.box;
78
+
79
+ console.log(`${cyan}${ml}${h.repeat(this.boxWidth)}${mr}${reset}`);
80
+ }
81
+
82
+ /**
83
+ * Print result line
84
+ */
85
+ printResultLine(name, status) {
86
+ const { cyan, green, red, yellow, dim, reset } = this.colors;
87
+ const { v } = this.box;
88
+ const { pass, fail, warn } = this.symbols;
89
+
90
+ let statusText, statusColor, symbol;
91
+
92
+ switch (status) {
93
+ case 'pass':
94
+ statusText = 'Pass';
95
+ statusColor = green;
96
+ symbol = pass;
97
+ break;
98
+ case 'fail':
99
+ statusText = 'Failed';
100
+ statusColor = red;
101
+ symbol = fail;
102
+ break;
103
+ case 'warn':
104
+ statusText = 'Warning';
105
+ statusColor = yellow;
106
+ symbol = warn;
107
+ break;
108
+ case 'skip':
109
+ statusText = 'Skipped';
110
+ statusColor = dim;
111
+ symbol = '-';
112
+ break;
113
+ default:
114
+ statusText = status;
115
+ statusColor = '';
116
+ symbol = '?';
117
+ }
118
+
119
+ const resultStr = `${symbol} [${statusText}]`;
120
+ const nameWidth = this.boxWidth - resultStr.length - 3;
121
+ const paddedName = name.substring(0, nameWidth).padEnd(nameWidth);
122
+
123
+ console.log(`${cyan}${v}${reset} ${paddedName}${statusColor}${resultStr}${reset} ${cyan}${v}${reset}`);
124
+ }
125
+
126
+ /**
127
+ * Print detail line
128
+ */
129
+ printDetail(message) {
130
+ const { cyan, dim, reset } = this.colors;
131
+ const { v, arrow } = this.box;
132
+
133
+ const indent = ` ${this.symbols.arrow} `;
134
+ const msgWidth = this.boxWidth - indent.length - 1;
135
+ const truncatedMsg = message.substring(0, msgWidth).padEnd(msgWidth);
136
+
137
+ console.log(`${cyan}${v}${reset}${dim}${indent}${truncatedMsg}${reset}${cyan}${v}${reset}`);
138
+ }
139
+
140
+ /**
141
+ * Print summary
142
+ */
143
+ printSummary(passed, total) {
144
+ const { cyan, green, red, yellow, bold, reset } = this.colors;
145
+ const { v } = this.box;
146
+
147
+ this.printDivider();
148
+
149
+ const summary = `Result: ${passed}/${total} checks passed`;
150
+ const padding = Math.floor((this.boxWidth - summary.length) / 2);
151
+ const paddingStr = ' '.repeat(padding);
152
+ const extraSpace = (this.boxWidth - summary.length) % 2 === 1 ? ' ' : '';
153
+
154
+ let color;
155
+ if (passed === total) {
156
+ color = green;
157
+ } else if (passed === 0) {
158
+ color = red;
159
+ } else {
160
+ color = yellow;
161
+ }
162
+
163
+ console.log(`${cyan}${v}${reset}${paddingStr}${color}${bold}${summary}${reset}${paddingStr}${extraSpace}${cyan}${v}${reset}`);
164
+ }
165
+
166
+ /**
167
+ * Log helpers
168
+ */
169
+ logInfo(message) {
170
+ const { blue, reset } = this.colors;
171
+ console.log(`${blue}${this.symbols.info}${reset} ${message}`);
172
+ }
173
+
174
+ logSuccess(message) {
175
+ const { green, reset } = this.colors;
176
+ console.log(`${green}${this.symbols.pass}${reset} ${message}`);
177
+ }
178
+
179
+ logError(message) {
180
+ const { red, reset } = this.colors;
181
+ console.log(`${red}${this.symbols.fail}${reset} ${message}`);
182
+ }
183
+
184
+ logWarn(message) {
185
+ const { yellow, reset } = this.colors;
186
+ console.log(`${yellow}${this.symbols.warn}${reset} ${message}`);
187
+ }
188
+ }
189
+
190
+ module.exports = Output;
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "sfdx-gatekeeper",
3
+ "version": "1.0.0",
4
+ "description": "A lightweight pre-commit validation framework for Salesforce projects",
5
+ "author": "Milanjeet Singh",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "git",
9
+ "hooks",
10
+ "pre-commit",
11
+ "commit-msg",
12
+ "salesforce",
13
+ "sfdx",
14
+ "lwc",
15
+ "apex",
16
+ "validation",
17
+ "linter",
18
+ "gatekeeper"
19
+ ],
20
+ "main": "src/index.js",
21
+ "bin": {
22
+ "sfdx-gatekeeper": "./bin/cli.js"
23
+ },
24
+ "files": [
25
+ "bin",
26
+ "src",
27
+ "lib",
28
+ "validations",
29
+ "templates"
30
+ ],
31
+ "scripts": {
32
+ "test": "node bin/cli.js test",
33
+ "lint": "eslint src/**/*.js"
34
+ },
35
+ "dependencies": {
36
+ "js-yaml": "^4.1.0"
37
+ },
38
+ "engines": {
39
+ "node": ">=14.0.0"
40
+ },
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://gitlab.com/milanjeet-singh/sfdx-gatekeeper.git"
44
+ },
45
+ "bugs": {
46
+ "url": "https://gitlab.com/milanjeet-singh/sfdx-gatekeeper/-/issues"
47
+ },
48
+ "homepage": "https://gitlab.com/milanjeet-singh/sfdx-gatekeeper#readme"
49
+ }
package/src/index.js ADDED
@@ -0,0 +1,440 @@
1
+ /**
2
+ * =============================================================================
3
+ * SFDX Gatekeeper - Main Module
4
+ *
5
+ * Author: Milanjeet Singh
6
+ * Created: March 2026
7
+ * =============================================================================
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { execSync } = require('child_process');
13
+ const yaml = require('js-yaml');
14
+ const Output = require('../lib/output.js');
15
+
16
+ class SFDXGatekeeper {
17
+ constructor(projectRoot = process.cwd()) {
18
+ this.projectRoot = projectRoot;
19
+ this.configPath = path.join(projectRoot, 'sfdx-gatekeeper.yml');
20
+ this.config = this.loadConfig();
21
+ this.output = new Output(this.config.settings?.boxWidth || 50);
22
+ this.results = [];
23
+ this.hookType = '';
24
+ this.commitMsgFile = '';
25
+ }
26
+
27
+ /**
28
+ * Load configuration from mtx-validator.yml
29
+ */
30
+ loadConfig() {
31
+ const defaultConfig = this.getDefaultConfig();
32
+
33
+ if (fs.existsSync(this.configPath)) {
34
+ try {
35
+ const userConfig = yaml.load(fs.readFileSync(this.configPath, 'utf8'));
36
+ return this.mergeConfig(defaultConfig, userConfig);
37
+ } catch (error) {
38
+ console.warn(`Warning: Could not parse config file: ${error.message}`);
39
+ return defaultConfig;
40
+ }
41
+ }
42
+
43
+ return defaultConfig;
44
+ }
45
+
46
+ /**
47
+ * Get default configuration
48
+ */
49
+ getDefaultConfig() {
50
+ return {
51
+ settings: {
52
+ name: 'SFDX Gatekeeper Validations',
53
+ boxWidth: 50
54
+ },
55
+ validations: {
56
+ enabled: ['commit-message', 'component-prefix', 'branch-naming'],
57
+ 'commit-message': {
58
+ prefix: 'Commit for',
59
+ issuePattern: '[IS]-[0-9]+',
60
+ blockedPhrases: ['initial commit', 'dummy', 'wip', 'fixup', 'squash'],
61
+ requireDescription: true,
62
+ validComponents: ['codvr', 'c_', 'lwc', 'aura', 'apex', 'flow', 'trigger', 'class', 'test', 'batch', 'scheduler', 'controller', 'handler', 'helper', 'service', 'util']
63
+ },
64
+ 'component-prefix': {
65
+ prefix: 'CODVR_',
66
+ checkTypes: ['lwc', 'aura', 'classes', 'triggers'],
67
+ paths: {
68
+ lwc: 'force-app/main/default/lwc',
69
+ aura: 'force-app/main/default/aura',
70
+ classes: 'force-app/main/default/classes',
71
+ triggers: 'force-app/main/default/triggers'
72
+ }
73
+ },
74
+ 'branch-naming': {
75
+ pattern: '^(feature|bugfix|hotfix|release|develop|main|master)/',
76
+ exempt: ['main', 'master', 'develop']
77
+ },
78
+ 'package-xml': {
79
+ path: 'manifest/package.xml',
80
+ enabled: false
81
+ }
82
+ }
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Deep merge configurations
88
+ */
89
+ mergeConfig(defaultConfig, userConfig) {
90
+ const merged = { ...defaultConfig };
91
+
92
+ if (userConfig.settings) {
93
+ merged.settings = { ...defaultConfig.settings, ...userConfig.settings };
94
+ }
95
+
96
+ if (userConfig.validations) {
97
+ merged.validations = { ...defaultConfig.validations };
98
+
99
+ if (userConfig.validations.enabled) {
100
+ merged.validations.enabled = userConfig.validations.enabled;
101
+ }
102
+
103
+ for (const key of Object.keys(userConfig.validations)) {
104
+ if (key !== 'enabled' && defaultConfig.validations[key]) {
105
+ merged.validations[key] = {
106
+ ...defaultConfig.validations[key],
107
+ ...userConfig.validations[key]
108
+ };
109
+ }
110
+ }
111
+ }
112
+
113
+ return merged;
114
+ }
115
+
116
+ /**
117
+ * Initialize config file in project
118
+ */
119
+ async init() {
120
+ const templatePath = path.join(__dirname, '..', 'templates', 'sfdx-gatekeeper.yml');
121
+
122
+ if (fs.existsSync(this.configPath)) {
123
+ console.log('⚠ sfdx-gatekeeper.yml already exists');
124
+ return;
125
+ }
126
+
127
+ let template;
128
+ if (fs.existsSync(templatePath)) {
129
+ template = fs.readFileSync(templatePath, 'utf8');
130
+ } else {
131
+ template = this.generateConfigTemplate();
132
+ }
133
+
134
+ fs.writeFileSync(this.configPath, template);
135
+ console.log('✔ Created sfdx-gatekeeper.yml');
136
+ console.log(' Edit this file to customize validations');
137
+ }
138
+
139
+ /**
140
+ * Generate config template
141
+ */
142
+ generateConfigTemplate() {
143
+ return `# =============================================================================
144
+ # SFDX Gatekeeper Configuration
145
+ # Author: Milanjeet Singh
146
+ # =============================================================================
147
+
148
+ settings:
149
+ name: "SFDX Gatekeeper Validations"
150
+ boxWidth: 50
151
+
152
+ validations:
153
+ # List of enabled validations
154
+ enabled:
155
+ - commit-message
156
+ - component-prefix
157
+ - branch-naming
158
+ # - package-xml # Uncomment to enable
159
+
160
+ # Commit message validation
161
+ commit-message:
162
+ prefix: "Commit for"
163
+ issuePattern: "[IS]-[0-9]+"
164
+ blockedPhrases:
165
+ - "initial commit"
166
+ - "dummy"
167
+ - "wip"
168
+ - "fixup"
169
+ - "squash"
170
+ requireDescription: true
171
+ validComponents:
172
+ - codvr
173
+ - lwc
174
+ - aura
175
+ - apex
176
+ - flow
177
+ - trigger
178
+ - class
179
+ - test
180
+ - batch
181
+ - controller
182
+ - handler
183
+ - service
184
+ - util
185
+
186
+ # Component prefix validation
187
+ component-prefix:
188
+ prefix: "CODVR_"
189
+ checkTypes:
190
+ - lwc
191
+ - aura
192
+ - classes
193
+ - triggers
194
+ paths:
195
+ lwc: "force-app/main/default/lwc"
196
+ aura: "force-app/main/default/aura"
197
+ classes: "force-app/main/default/classes"
198
+ triggers: "force-app/main/default/triggers"
199
+
200
+ # Branch naming validation
201
+ branch-naming:
202
+ pattern: "^(feature|bugfix|hotfix|release|develop|main|master)/"
203
+ exempt:
204
+ - main
205
+ - master
206
+ - develop
207
+
208
+ # Package XML sync validation
209
+ package-xml:
210
+ path: "manifest/package.xml"
211
+ enabled: false
212
+ `;
213
+ }
214
+
215
+ /**
216
+ * Install git hooks
217
+ */
218
+ async installHooks() {
219
+ const gitDir = path.join(this.projectRoot, '.git');
220
+
221
+ if (!fs.existsSync(gitDir)) {
222
+ throw new Error('Not a git repository');
223
+ }
224
+
225
+ const hooksDir = path.join(gitDir, 'hooks');
226
+ if (!fs.existsSync(hooksDir)) {
227
+ fs.mkdirSync(hooksDir, { recursive: true });
228
+ }
229
+
230
+ // Pre-commit hook
231
+ const preCommitHook = `#!/usr/bin/env sh
232
+ npx sfdx-gatekeeper run pre-commit
233
+ `;
234
+ fs.writeFileSync(path.join(hooksDir, 'pre-commit'), preCommitHook, { mode: 0o755 });
235
+
236
+ // Commit-msg hook
237
+ const commitMsgHook = `#!/usr/bin/env sh
238
+ npx sfdx-gatekeeper run commit-msg "$1"
239
+ `;
240
+ fs.writeFileSync(path.join(hooksDir, 'commit-msg'), commitMsgHook, { mode: 0o755 });
241
+
242
+ // Pre-push hook
243
+ const prePushHook = `#!/usr/bin/env sh
244
+ npx sfdx-gatekeeper run pre-push
245
+ `;
246
+ fs.writeFileSync(path.join(hooksDir, 'pre-push'), prePushHook, { mode: 0o755 });
247
+
248
+ console.log('✔ Git hooks installed successfully!');
249
+ console.log(' Hooks installed in: ' + hooksDir);
250
+ }
251
+
252
+ /**
253
+ * Uninstall git hooks
254
+ */
255
+ async uninstallHooks() {
256
+ const hooksDir = path.join(this.projectRoot, '.git', 'hooks');
257
+
258
+ for (const hook of ['pre-commit', 'commit-msg', 'pre-push']) {
259
+ const hookPath = path.join(hooksDir, hook);
260
+ if (fs.existsSync(hookPath)) {
261
+ fs.unlinkSync(hookPath);
262
+ console.log(` Removed: ${hook}`);
263
+ }
264
+ }
265
+
266
+ console.log('✔ Git hooks uninstalled!');
267
+ }
268
+
269
+ /**
270
+ * Run validations for a hook
271
+ */
272
+ async run(hook, hookArg) {
273
+ this.hookType = hook;
274
+ this.results = [];
275
+
276
+ switch (hook) {
277
+ case 'pre-commit':
278
+ await this.runPreCommit();
279
+ break;
280
+ case 'commit-msg':
281
+ this.commitMsgFile = hookArg;
282
+ await this.runCommitMsg();
283
+ break;
284
+ case 'pre-push':
285
+ await this.runPrePush();
286
+ break;
287
+ default:
288
+ throw new Error(`Unknown hook: ${hook}`);
289
+ }
290
+
291
+ this.displayResults();
292
+
293
+ const failedCount = this.results.filter(r => r.status === 'fail').length;
294
+ return failedCount > 0 ? 1 : 0;
295
+ }
296
+
297
+ /**
298
+ * Run pre-commit validations
299
+ */
300
+ async runPreCommit() {
301
+ const enabled = this.config.validations.enabled;
302
+
303
+ for (const name of enabled) {
304
+ if (name === 'commit-message') continue; // Runs in commit-msg hook
305
+ await this.runValidation(name);
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Run commit-msg validations
311
+ */
312
+ async runCommitMsg() {
313
+ await this.runValidation('commit-message');
314
+ }
315
+
316
+ /**
317
+ * Run pre-push validations
318
+ */
319
+ async runPrePush() {
320
+ await this.runValidation('branch-naming');
321
+ }
322
+
323
+ /**
324
+ * Run a single validation
325
+ */
326
+ async runValidation(name) {
327
+ const validationPath = path.join(__dirname, '..', 'validations', `${name}.js`);
328
+
329
+ if (!fs.existsSync(validationPath)) {
330
+ console.warn(`Warning: Validation not found: ${name}`);
331
+ return;
332
+ }
333
+
334
+ const Validation = require(validationPath);
335
+ const validation = new Validation(this);
336
+
337
+ try {
338
+ const result = await validation.run();
339
+ this.results.push({
340
+ name: validation.displayName,
341
+ status: result.status,
342
+ details: result.details
343
+ });
344
+ } catch (error) {
345
+ this.results.push({
346
+ name: name,
347
+ status: 'fail',
348
+ details: error.message
349
+ });
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Display results
355
+ */
356
+ displayResults() {
357
+ const passed = this.results.filter(r => r.status === 'pass').length;
358
+ const total = this.results.length;
359
+
360
+ this.output.printHeader(this.config.settings.name);
361
+
362
+ for (const result of this.results) {
363
+ this.output.printResultLine(result.name, result.status);
364
+ if (result.details && result.status === 'fail') {
365
+ this.output.printDetail(result.details);
366
+ }
367
+ }
368
+
369
+ this.output.printSummary(passed, total);
370
+ this.output.printFooter();
371
+ }
372
+
373
+ /**
374
+ * List available validations
375
+ */
376
+ listValidations() {
377
+ const validationsDir = path.join(__dirname, '..', 'validations');
378
+ const enabled = this.config.validations.enabled;
379
+
380
+ console.log('Available validations:\n');
381
+
382
+ if (fs.existsSync(validationsDir)) {
383
+ const files = fs.readdirSync(validationsDir).filter(f => f.endsWith('.js'));
384
+
385
+ for (const file of files) {
386
+ const name = path.basename(file, '.js');
387
+ const marker = enabled.includes(name) ? '*' : ' ';
388
+ console.log(` [${marker}] ${name}`);
389
+ }
390
+ }
391
+
392
+ console.log('\n [*] = enabled in sfdx-gatekeeper.yml');
393
+ }
394
+
395
+ /**
396
+ * Test output formatting
397
+ */
398
+ testOutput() {
399
+ this.output.printHeader(this.config.settings.name);
400
+ this.output.printResultLine('Commit Message Standard', 'pass');
401
+ this.output.printResultLine('Package XML in Sync', 'pass');
402
+ this.output.printResultLine('Component Prefix (CODVR_)', 'fail');
403
+ this.output.printDetail('myComponent missing CODVR_ prefix');
404
+ this.output.printResultLine('Branch Naming Convention', 'pass');
405
+ this.output.printSummary(3, 4);
406
+ this.output.printFooter();
407
+ }
408
+
409
+ /**
410
+ * Get staged files
411
+ */
412
+ getStagedFiles() {
413
+ try {
414
+ const result = execSync('git diff --cached --name-only --diff-filter=ACM', {
415
+ cwd: this.projectRoot,
416
+ encoding: 'utf8'
417
+ });
418
+ return result.trim().split('\n').filter(f => f);
419
+ } catch (error) {
420
+ return [];
421
+ }
422
+ }
423
+
424
+ /**
425
+ * Get current branch name
426
+ */
427
+ getCurrentBranch() {
428
+ try {
429
+ const result = execSync('git rev-parse --abbrev-ref HEAD', {
430
+ cwd: this.projectRoot,
431
+ encoding: 'utf8'
432
+ });
433
+ return result.trim();
434
+ } catch (error) {
435
+ return '';
436
+ }
437
+ }
438
+ }
439
+
440
+ module.exports = { SFDXGatekeeper };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * =============================================================================
3
+ * Validation: Branch Naming Convention
4
+ * Ensures branch names follow the standard pattern
5
+ *
6
+ * Author: Milanjeet Singh
7
+ * Created: March 2026
8
+ * =============================================================================
9
+ */
10
+
11
+ class BranchNamingValidation {
12
+ constructor(validator) {
13
+ this.validator = validator;
14
+ this.config = validator.config.validations['branch-naming'];
15
+ this.displayName = 'Branch Naming Convention';
16
+ }
17
+
18
+ async run() {
19
+ const branch = this.validator.getCurrentBranch();
20
+
21
+ if (!branch) {
22
+ return {
23
+ status: 'fail',
24
+ details: 'Could not determine branch name'
25
+ };
26
+ }
27
+
28
+ // Check if branch is exempt
29
+ const exempt = this.config.exempt || [];
30
+ if (exempt.includes(branch)) {
31
+ return { status: 'pass', details: '' };
32
+ }
33
+
34
+ // Check if branch matches pattern
35
+ const pattern = new RegExp(this.config.pattern);
36
+ if (pattern.test(branch)) {
37
+ return { status: 'pass', details: '' };
38
+ }
39
+
40
+ return {
41
+ status: 'fail',
42
+ details: `'${branch}' doesn't match pattern`
43
+ };
44
+ }
45
+ }
46
+
47
+ module.exports = BranchNamingValidation;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * =============================================================================
3
+ * Validation: Commit Message Standard
4
+ *
5
+ * Author: Milanjeet Singh
6
+ * Created: March 2026
7
+ * =============================================================================
8
+ */
9
+
10
+ const fs = require('fs');
11
+
12
+ class CommitMessageValidation {
13
+ constructor(validator) {
14
+ this.validator = validator;
15
+ this.config = validator.config.validations['commit-message'];
16
+ this.displayName = 'Commit Message Standard';
17
+ }
18
+
19
+ async run() {
20
+ const commitMsgFile = this.validator.commitMsgFile;
21
+
22
+ // Read commit message
23
+ let commitMsg = '';
24
+ if (commitMsgFile && fs.existsSync(commitMsgFile)) {
25
+ commitMsg = fs.readFileSync(commitMsgFile, 'utf8').trim();
26
+ }
27
+
28
+ // Check for empty message
29
+ if (!commitMsg) {
30
+ return {
31
+ status: 'fail',
32
+ details: 'Commit message is empty'
33
+ };
34
+ }
35
+
36
+ // Check for blocked phrases
37
+ const blockedPhrases = this.config.blockedPhrases || [];
38
+ for (const phrase of blockedPhrases) {
39
+ if (commitMsg.toLowerCase().includes(phrase.toLowerCase())) {
40
+ return {
41
+ status: 'fail',
42
+ details: `Contains blocked phrase: "${phrase}"`
43
+ };
44
+ }
45
+ }
46
+
47
+ // Check for required prefix
48
+ const prefix = this.config.prefix;
49
+ if (prefix && !commitMsg.startsWith(prefix + ' ')) {
50
+ return {
51
+ status: 'fail',
52
+ details: `Must start with "${prefix}"`
53
+ };
54
+ }
55
+
56
+ // Check for Issue/Story number
57
+ const issuePattern = new RegExp(this.config.issuePattern);
58
+ if (issuePattern.test(commitMsg)) {
59
+ return { status: 'pass', details: '' };
60
+ }
61
+
62
+ // Check for valid component name
63
+ const afterPrefix = commitMsg.substring(prefix.length + 1);
64
+ const component = afterPrefix.split(' ')[0];
65
+
66
+ const validComponents = this.config.validComponents || [];
67
+ const isValidComponent = validComponents.some(vc =>
68
+ component.toLowerCase().startsWith(vc.toLowerCase())
69
+ );
70
+
71
+ if (isValidComponent) {
72
+ // Check for description after dash
73
+ if (this.config.requireDescription) {
74
+ const hasDescription = /^[^ ]+ - .+/.test(afterPrefix);
75
+ if (!hasDescription) {
76
+ return {
77
+ status: 'fail',
78
+ details: "Missing description after ' - '"
79
+ };
80
+ }
81
+ }
82
+ return { status: 'pass', details: '' };
83
+ }
84
+
85
+ // No valid pattern found
86
+ return {
87
+ status: 'fail',
88
+ details: 'Need I-xxx, S-xxx, or component name'
89
+ };
90
+ }
91
+ }
92
+
93
+ module.exports = CommitMessageValidation;
@@ -0,0 +1,124 @@
1
+ /**
2
+ * =============================================================================
3
+ * Validation: Component Prefix
4
+ * Ensures all components have the required prefix (e.g., CODVR_)
5
+ *
6
+ * Author: Milanjeet Singh
7
+ * Created: March 2026
8
+ * =============================================================================
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ class ComponentPrefixValidation {
15
+ constructor(validator) {
16
+ this.validator = validator;
17
+ this.config = validator.config.validations['component-prefix'];
18
+ this.prefix = this.config.prefix || 'CODVR_';
19
+ this.displayName = `Component Prefix (${this.prefix})`;
20
+ }
21
+
22
+ async run() {
23
+ const invalidComponents = [];
24
+ const stagedFiles = this.validator.getStagedFiles();
25
+ const projectRoot = this.validator.projectRoot;
26
+ const checkTypes = this.config.checkTypes || [];
27
+ const paths = this.config.paths || {};
28
+
29
+ // Check LWC components
30
+ if (checkTypes.includes('lwc')) {
31
+ const lwcPath = path.join(projectRoot, paths.lwc || 'force-app/main/default/lwc');
32
+ if (fs.existsSync(lwcPath)) {
33
+ const components = fs.readdirSync(lwcPath, { withFileTypes: true })
34
+ .filter(d => d.isDirectory())
35
+ .map(d => d.name);
36
+
37
+ for (const name of components) {
38
+ if (!this.hasValidPrefix(name)) {
39
+ // Only check staged components
40
+ const isStaged = stagedFiles.some(f => f.includes(`/lwc/${name}/`));
41
+ if (stagedFiles.length === 0 || isStaged) {
42
+ invalidComponents.push(`${name} (LWC)`);
43
+ }
44
+ }
45
+ }
46
+ }
47
+ }
48
+
49
+ // Check Aura components
50
+ if (checkTypes.includes('aura')) {
51
+ const auraPath = path.join(projectRoot, paths.aura || 'force-app/main/default/aura');
52
+ if (fs.existsSync(auraPath)) {
53
+ const components = fs.readdirSync(auraPath, { withFileTypes: true })
54
+ .filter(d => d.isDirectory())
55
+ .map(d => d.name);
56
+
57
+ for (const name of components) {
58
+ if (!this.hasValidPrefix(name)) {
59
+ const isStaged = stagedFiles.some(f => f.includes(`/aura/${name}/`));
60
+ if (stagedFiles.length === 0 || isStaged) {
61
+ invalidComponents.push(`${name} (Aura)`);
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ // Check Apex classes
69
+ if (checkTypes.includes('classes')) {
70
+ const classesPath = path.join(projectRoot, paths.classes || 'force-app/main/default/classes');
71
+ if (fs.existsSync(classesPath)) {
72
+ const classes = fs.readdirSync(classesPath)
73
+ .filter(f => f.endsWith('.cls'))
74
+ .map(f => f.replace('.cls', ''));
75
+
76
+ for (const name of classes) {
77
+ if (!this.hasValidPrefix(name)) {
78
+ const isStaged = stagedFiles.some(f => f.includes(`/classes/${name}.cls`));
79
+ if (stagedFiles.length === 0 || isStaged) {
80
+ invalidComponents.push(`${name} (Apex)`);
81
+ }
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ // Check Triggers
88
+ if (checkTypes.includes('triggers')) {
89
+ const triggersPath = path.join(projectRoot, paths.triggers || 'force-app/main/default/triggers');
90
+ if (fs.existsSync(triggersPath)) {
91
+ const triggers = fs.readdirSync(triggersPath)
92
+ .filter(f => f.endsWith('.trigger'))
93
+ .map(f => f.replace('.trigger', ''));
94
+
95
+ for (const name of triggers) {
96
+ if (!this.hasValidPrefix(name)) {
97
+ const isStaged = stagedFiles.some(f => f.includes(`/triggers/${name}.trigger`));
98
+ if (stagedFiles.length === 0 || isStaged) {
99
+ invalidComponents.push(`${name} (Trigger)`);
100
+ }
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ if (invalidComponents.length === 0) {
107
+ return { status: 'pass', details: '' };
108
+ }
109
+
110
+ return {
111
+ status: 'fail',
112
+ details: invalidComponents.slice(0, 3).join(', ') +
113
+ (invalidComponents.length > 3 ? ` (+${invalidComponents.length - 3} more)` : '')
114
+ };
115
+ }
116
+
117
+ hasValidPrefix(name) {
118
+ const prefix = this.prefix.toLowerCase();
119
+ return name.toLowerCase().startsWith(prefix) ||
120
+ name.toLowerCase().startsWith('c' + prefix.substring(1)); // cODVR_ format
121
+ }
122
+ }
123
+
124
+ module.exports = ComponentPrefixValidation;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * =============================================================================
3
+ * Validation: Package XML Sync
4
+ * Checks if package.xml is in sync with force-app components
5
+ *
6
+ * Author: Milanjeet Singh
7
+ * Created: March 2026
8
+ * =============================================================================
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ class PackageXmlValidation {
15
+ constructor(validator) {
16
+ this.validator = validator;
17
+ this.config = validator.config.validations['package-xml'];
18
+ this.displayName = 'Package XML in Sync';
19
+ }
20
+
21
+ async run() {
22
+ const packageXmlPath = path.join(
23
+ this.validator.projectRoot,
24
+ this.config.path || 'manifest/package.xml'
25
+ );
26
+
27
+ // Check if package.xml exists
28
+ if (!fs.existsSync(packageXmlPath)) {
29
+ return {
30
+ status: 'warn',
31
+ details: 'package.xml not found'
32
+ };
33
+ }
34
+
35
+ if (!this.config.enabled) {
36
+ return { status: 'skip', details: '' };
37
+ }
38
+
39
+ const packageXmlContent = fs.readFileSync(packageXmlPath, 'utf8');
40
+ const stagedFiles = this.validator.getStagedFiles();
41
+ const missingComponents = [];
42
+
43
+ // Check staged Apex classes
44
+ const stagedClasses = stagedFiles
45
+ .filter(f => f.endsWith('.cls') && !f.endsWith('-meta.xml'))
46
+ .map(f => path.basename(f, '.cls'));
47
+
48
+ for (const className of stagedClasses) {
49
+ if (!packageXmlContent.includes(`<members>${className}</members>`)) {
50
+ missingComponents.push(className);
51
+ }
52
+ }
53
+
54
+ // Check staged LWC components
55
+ const stagedLwc = [...new Set(
56
+ stagedFiles
57
+ .filter(f => f.includes('/lwc/'))
58
+ .map(f => {
59
+ const match = f.match(/\/lwc\/([^/]+)\//);
60
+ return match ? match[1] : null;
61
+ })
62
+ .filter(Boolean)
63
+ )];
64
+
65
+ for (const lwcName of stagedLwc) {
66
+ if (!packageXmlContent.includes(`<members>${lwcName}</members>`)) {
67
+ missingComponents.push(lwcName);
68
+ }
69
+ }
70
+
71
+ if (missingComponents.length === 0) {
72
+ return { status: 'pass', details: '' };
73
+ }
74
+
75
+ return {
76
+ status: 'fail',
77
+ details: `Missing: ${missingComponents.slice(0, 3).join(', ')}` +
78
+ (missingComponents.length > 3 ? ` (+${missingComponents.length - 3} more)` : '')
79
+ };
80
+ }
81
+ }
82
+
83
+ module.exports = PackageXmlValidation;