@hatem427/code-guard-ci 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/.husky/pre-commit +27 -0
- package/LICENSE +21 -0
- package/README.md +646 -0
- package/config/angular.config.ts +223 -0
- package/config/guidelines.config.ts +229 -0
- package/config/nextjs.config.ts +160 -0
- package/config/react.config.ts +330 -0
- package/dist/config/angular.config.d.ts +15 -0
- package/dist/config/angular.config.d.ts.map +1 -0
- package/dist/config/angular.config.js +187 -0
- package/dist/config/angular.config.js.map +1 -0
- package/dist/config/guidelines.config.d.ts +63 -0
- package/dist/config/guidelines.config.d.ts.map +1 -0
- package/dist/config/guidelines.config.js +167 -0
- package/dist/config/guidelines.config.js.map +1 -0
- package/dist/config/nextjs.config.d.ts +18 -0
- package/dist/config/nextjs.config.d.ts.map +1 -0
- package/dist/config/nextjs.config.js +133 -0
- package/dist/config/nextjs.config.js.map +1 -0
- package/dist/config/react.config.d.ts +15 -0
- package/dist/config/react.config.d.ts.map +1 -0
- package/dist/config/react.config.js +287 -0
- package/dist/config/react.config.js.map +1 -0
- package/dist/scripts/auto-fix.d.ts +16 -0
- package/dist/scripts/auto-fix.d.ts.map +1 -0
- package/dist/scripts/auto-fix.js +130 -0
- package/dist/scripts/auto-fix.js.map +1 -0
- package/dist/scripts/cli.d.ts +17 -0
- package/dist/scripts/cli.d.ts.map +1 -0
- package/dist/scripts/cli.js +255 -0
- package/dist/scripts/cli.js.map +1 -0
- package/dist/scripts/delete-bypass-logs.d.ts +17 -0
- package/dist/scripts/delete-bypass-logs.d.ts.map +1 -0
- package/dist/scripts/delete-bypass-logs.js +242 -0
- package/dist/scripts/delete-bypass-logs.js.map +1 -0
- package/dist/scripts/generate-doc.d.ts +18 -0
- package/dist/scripts/generate-doc.d.ts.map +1 -0
- package/dist/scripts/generate-doc.js +300 -0
- package/dist/scripts/generate-doc.js.map +1 -0
- package/dist/scripts/generate-pr-checklist.d.ts +20 -0
- package/dist/scripts/generate-pr-checklist.d.ts.map +1 -0
- package/dist/scripts/generate-pr-checklist.js +276 -0
- package/dist/scripts/generate-pr-checklist.js.map +1 -0
- package/dist/scripts/precommit-check.d.ts +23 -0
- package/dist/scripts/precommit-check.d.ts.map +1 -0
- package/dist/scripts/precommit-check.js +331 -0
- package/dist/scripts/precommit-check.js.map +1 -0
- package/dist/scripts/set-admin-password.d.ts +14 -0
- package/dist/scripts/set-admin-password.d.ts.map +1 -0
- package/dist/scripts/set-admin-password.js +116 -0
- package/dist/scripts/set-admin-password.js.map +1 -0
- package/dist/scripts/set-bypass-password.d.ts +11 -0
- package/dist/scripts/set-bypass-password.d.ts.map +1 -0
- package/dist/scripts/set-bypass-password.js +106 -0
- package/dist/scripts/set-bypass-password.js.map +1 -0
- package/dist/scripts/utils/auto-fixer.d.ts +28 -0
- package/dist/scripts/utils/auto-fixer.d.ts.map +1 -0
- package/dist/scripts/utils/auto-fixer.js +177 -0
- package/dist/scripts/utils/auto-fixer.js.map +1 -0
- package/dist/scripts/utils/bypass-manager.d.ts +101 -0
- package/dist/scripts/utils/bypass-manager.d.ts.map +1 -0
- package/dist/scripts/utils/bypass-manager.js +496 -0
- package/dist/scripts/utils/bypass-manager.js.map +1 -0
- package/dist/scripts/utils/code-analyzer.d.ts +34 -0
- package/dist/scripts/utils/code-analyzer.d.ts.map +1 -0
- package/dist/scripts/utils/code-analyzer.js +323 -0
- package/dist/scripts/utils/code-analyzer.js.map +1 -0
- package/dist/scripts/utils/file-checker.d.ts +93 -0
- package/dist/scripts/utils/file-checker.d.ts.map +1 -0
- package/dist/scripts/utils/file-checker.js +248 -0
- package/dist/scripts/utils/file-checker.js.map +1 -0
- package/dist/scripts/utils/logger.d.ts +26 -0
- package/dist/scripts/utils/logger.d.ts.map +1 -0
- package/dist/scripts/utils/logger.js +86 -0
- package/dist/scripts/utils/logger.js.map +1 -0
- package/dist/scripts/utils/project-detector.d.ts +34 -0
- package/dist/scripts/utils/project-detector.d.ts.map +1 -0
- package/dist/scripts/utils/project-detector.js +124 -0
- package/dist/scripts/utils/project-detector.js.map +1 -0
- package/dist/scripts/utils/rule-engine.d.ts +57 -0
- package/dist/scripts/utils/rule-engine.d.ts.map +1 -0
- package/dist/scripts/utils/rule-engine.js +158 -0
- package/dist/scripts/utils/rule-engine.js.map +1 -0
- package/dist/scripts/view-bypass-log.d.ts +13 -0
- package/dist/scripts/view-bypass-log.d.ts.map +1 -0
- package/dist/scripts/view-bypass-log.js +117 -0
- package/dist/scripts/view-bypass-log.js.map +1 -0
- package/package.json +74 -0
- package/scripts/auto-fix.ts +115 -0
- package/scripts/cli.ts +246 -0
- package/scripts/delete-bypass-logs.ts +253 -0
- package/scripts/generate-doc.ts +317 -0
- package/scripts/generate-pr-checklist.ts +285 -0
- package/scripts/precommit-check.ts +349 -0
- package/scripts/set-admin-password.ts +90 -0
- package/scripts/set-bypass-password.ts +80 -0
- package/scripts/utils/auto-fixer.ts +181 -0
- package/scripts/utils/bypass-manager.ts +566 -0
- package/scripts/utils/code-analyzer.ts +341 -0
- package/scripts/utils/file-checker.ts +253 -0
- package/scripts/utils/logger.ts +88 -0
- package/scripts/utils/project-detector.ts +115 -0
- package/scripts/utils/rule-engine.ts +186 -0
- package/scripts/view-bypass-log.ts +92 -0
- package/templates/feature-doc-api.md +101 -0
- package/templates/feature-doc-service.md +113 -0
- package/templates/feature-doc-ui.md +91 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* generate-pr-checklist.ts — PR Checklist Generator
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* Generates a Markdown checklist suitable for pasting into a GitHub PR
|
|
7
|
+
* description or comment. The checklist includes:
|
|
8
|
+
* - All active coding rules (grouped by category)
|
|
9
|
+
* - Feature doc verification
|
|
10
|
+
* - Pre-commit check result summary
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* npm run generate-pr-checklist
|
|
14
|
+
* npm run generate-pr-checklist -- --output=pr-checklist.md
|
|
15
|
+
* npm run generate-pr-checklist -- --project=angular
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import * as path from 'path';
|
|
20
|
+
import { execSync } from 'child_process';
|
|
21
|
+
|
|
22
|
+
import { detectProject, ProjectType } from './utils/project-detector';
|
|
23
|
+
import { getRulesForProject, getAllRules, Rule, Severity } from '../config/guidelines.config';
|
|
24
|
+
import { getCurrentBranch } from './utils/file-checker';
|
|
25
|
+
import * as logger from './utils/logger';
|
|
26
|
+
|
|
27
|
+
// ── Load all project-type configs (side-effect: registers rules) ────────────
|
|
28
|
+
|
|
29
|
+
import '../config/angular.config';
|
|
30
|
+
import '../config/react.config';
|
|
31
|
+
import '../config/nextjs.config';
|
|
32
|
+
|
|
33
|
+
// ── Types ───────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
interface ChecklistOptions {
|
|
36
|
+
/** Override project type instead of auto-detecting */
|
|
37
|
+
project?: ProjectType;
|
|
38
|
+
/** Output file path (if not set, prints to stdout) */
|
|
39
|
+
output?: string;
|
|
40
|
+
/** Include all rules across all project types */
|
|
41
|
+
all?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Argument parsing ────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
function parseArgs(): ChecklistOptions {
|
|
47
|
+
const args = process.argv.slice(2);
|
|
48
|
+
const opts: ChecklistOptions = {};
|
|
49
|
+
|
|
50
|
+
for (const arg of args) {
|
|
51
|
+
const [key, ...valueParts] = arg.replace(/^--/, '').split('=');
|
|
52
|
+
const value = valueParts.join('=').replace(/^["']|["']$/g, '');
|
|
53
|
+
|
|
54
|
+
switch (key) {
|
|
55
|
+
case 'project':
|
|
56
|
+
if (['angular', 'react', 'nextjs'].includes(value)) {
|
|
57
|
+
opts.project = value as ProjectType;
|
|
58
|
+
}
|
|
59
|
+
break;
|
|
60
|
+
case 'output':
|
|
61
|
+
opts.output = value;
|
|
62
|
+
break;
|
|
63
|
+
case 'all':
|
|
64
|
+
opts.all = value === 'true' || value === '';
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return opts;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Checklist generation ────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get the severity emoji for display.
|
|
76
|
+
*/
|
|
77
|
+
function severityEmoji(severity: Severity): string {
|
|
78
|
+
switch (severity) {
|
|
79
|
+
case 'error':
|
|
80
|
+
return '🔴';
|
|
81
|
+
case 'warning':
|
|
82
|
+
return '🟡';
|
|
83
|
+
case 'info':
|
|
84
|
+
return '🔵';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if feature documentation exists for the current branch.
|
|
90
|
+
*/
|
|
91
|
+
function checkFeatureDocs(): { exists: boolean; featureName: string; docPath: string } {
|
|
92
|
+
const branch = getCurrentBranch();
|
|
93
|
+
const featureName = branch.replace(/^(feat|feature)\//, '');
|
|
94
|
+
const docPath = path.join('docs', 'features', `${featureName}.md`);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
exists: fs.existsSync(docPath),
|
|
98
|
+
featureName,
|
|
99
|
+
docPath,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get the list of changed files for this PR (compared to main/master).
|
|
105
|
+
*/
|
|
106
|
+
function getChangedFiles(): string[] {
|
|
107
|
+
try {
|
|
108
|
+
// Try comparing with main first, then master
|
|
109
|
+
const baseBranch = (() => {
|
|
110
|
+
try {
|
|
111
|
+
execSync('git rev-parse --verify origin/main', { stdio: 'pipe' });
|
|
112
|
+
return 'origin/main';
|
|
113
|
+
} catch {
|
|
114
|
+
return 'origin/master';
|
|
115
|
+
}
|
|
116
|
+
})();
|
|
117
|
+
|
|
118
|
+
const output = execSync(`git diff --name-only ${baseBranch}...HEAD`, {
|
|
119
|
+
encoding: 'utf-8',
|
|
120
|
+
}).trim();
|
|
121
|
+
|
|
122
|
+
return output ? output.split('\n').filter(Boolean) : [];
|
|
123
|
+
} catch {
|
|
124
|
+
// Fallback: just get staged files
|
|
125
|
+
try {
|
|
126
|
+
return execSync('git diff --cached --name-only', { encoding: 'utf-8' })
|
|
127
|
+
.trim()
|
|
128
|
+
.split('\n')
|
|
129
|
+
.filter(Boolean);
|
|
130
|
+
} catch {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Generate the full PR checklist in Markdown format.
|
|
138
|
+
*/
|
|
139
|
+
function generateChecklist(rules: Rule[], projectLabel: string): string {
|
|
140
|
+
const branch = getCurrentBranch();
|
|
141
|
+
const changedFiles = getChangedFiles();
|
|
142
|
+
const featureDoc = checkFeatureDocs();
|
|
143
|
+
const isFeatureBranch = branch.startsWith('feat/') || branch.startsWith('feature/');
|
|
144
|
+
const today = new Date().toISOString().split('T')[0];
|
|
145
|
+
|
|
146
|
+
let md = '';
|
|
147
|
+
|
|
148
|
+
// ── Header ──────────────────────────────────────────────────────────────
|
|
149
|
+
md += `## 🛡️ Code Guardian — PR Checklist\n\n`;
|
|
150
|
+
md += `> Auto-generated on ${today} for **${projectLabel}** project\n`;
|
|
151
|
+
md += `> Branch: \`${branch}\` | Files changed: ${changedFiles.length}\n\n`;
|
|
152
|
+
|
|
153
|
+
// ── Changed files summary ─────────────────────────────────────────────
|
|
154
|
+
md += `### 📁 Changed Files (${changedFiles.length})\n\n`;
|
|
155
|
+
if (changedFiles.length > 0) {
|
|
156
|
+
md += `<details>\n<summary>Click to expand file list</summary>\n\n`;
|
|
157
|
+
for (const f of changedFiles) {
|
|
158
|
+
md += `- \`${f}\`\n`;
|
|
159
|
+
}
|
|
160
|
+
md += `\n</details>\n\n`;
|
|
161
|
+
} else {
|
|
162
|
+
md += `_No changed files detected._\n\n`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── Coding rules checklist ────────────────────────────────────────────
|
|
166
|
+
md += `### ✅ Coding Rules Checklist\n\n`;
|
|
167
|
+
|
|
168
|
+
// Group rules by category
|
|
169
|
+
const byCategory = new Map<string, Rule[]>();
|
|
170
|
+
for (const rule of rules) {
|
|
171
|
+
const existing = byCategory.get(rule.category) || [];
|
|
172
|
+
existing.push(rule);
|
|
173
|
+
byCategory.set(rule.category, existing);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const [category, categoryRules] of byCategory) {
|
|
177
|
+
md += `**${category}**\n\n`;
|
|
178
|
+
|
|
179
|
+
for (const rule of categoryRules) {
|
|
180
|
+
const emoji = severityEmoji(rule.severity);
|
|
181
|
+
md += `- [ ] ${emoji} **${rule.label}** — ${rule.description}\n`;
|
|
182
|
+
}
|
|
183
|
+
md += `\n`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── Feature docs section ──────────────────────────────────────────────
|
|
187
|
+
md += `### 📄 Documentation\n\n`;
|
|
188
|
+
|
|
189
|
+
if (isFeatureBranch) {
|
|
190
|
+
const docStatus = featureDoc.exists ? '✅' : '❌';
|
|
191
|
+
md += `- [${featureDoc.exists ? 'x' : ' '}] ${docStatus} Feature documentation exists at \`${featureDoc.docPath}\`\n`;
|
|
192
|
+
|
|
193
|
+
if (!featureDoc.exists) {
|
|
194
|
+
md += ` > ⚠️ **Action required**: Run \`npm run generate-doc -- --name="${featureDoc.featureName}" --type=ui\` to generate docs.\n`;
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
md += `- [x] ℹ️ Not a feature branch — docs not required.\n`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
md += `\n`;
|
|
201
|
+
|
|
202
|
+
// ── Pre-commit verification ───────────────────────────────────────────
|
|
203
|
+
md += `### 🔧 Pre-commit Checks\n\n`;
|
|
204
|
+
md += `- [ ] ✅ \`npm run precommit-check\` passes without errors\n`;
|
|
205
|
+
md += `- [ ] ✅ ESLint reports no errors\n`;
|
|
206
|
+
md += `- [ ] ✅ Prettier formatting is consistent\n`;
|
|
207
|
+
md += `- [ ] ✅ No console.log statements in committed code\n`;
|
|
208
|
+
md += `- [ ] ✅ All components ≤ 400 lines\n`;
|
|
209
|
+
md += `\n`;
|
|
210
|
+
|
|
211
|
+
// ── Additional review items ───────────────────────────────────────────
|
|
212
|
+
md += `### 🔍 Manual Review Points\n\n`;
|
|
213
|
+
md += `- [ ] Code has been self-reviewed\n`;
|
|
214
|
+
md += `- [ ] Naming conventions are followed\n`;
|
|
215
|
+
md += `- [ ] No hardcoded values or magic numbers\n`;
|
|
216
|
+
md += `- [ ] Error handling is in place\n`;
|
|
217
|
+
md += `- [ ] Edge cases are considered\n`;
|
|
218
|
+
md += `- [ ] Tests are updated/added for new functionality\n`;
|
|
219
|
+
md += `- [ ] No unnecessary comments or dead code\n`;
|
|
220
|
+
md += `\n`;
|
|
221
|
+
|
|
222
|
+
// ── Footer ────────────────────────────────────────────────────────────
|
|
223
|
+
md += `---\n`;
|
|
224
|
+
md += `_Generated by [Code Guardian](https://github.com/team/code-guardian) v1.0.0_\n`;
|
|
225
|
+
|
|
226
|
+
return md;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ── Main ────────────────────────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
function main(): void {
|
|
232
|
+
logger.header('📋 Code Guardian — PR Checklist Generator');
|
|
233
|
+
|
|
234
|
+
const opts = parseArgs();
|
|
235
|
+
|
|
236
|
+
// Determine project type
|
|
237
|
+
let projectType: ProjectType;
|
|
238
|
+
let projectLabel: string;
|
|
239
|
+
|
|
240
|
+
if (opts.project) {
|
|
241
|
+
projectType = opts.project;
|
|
242
|
+
projectLabel = opts.project.charAt(0).toUpperCase() + opts.project.slice(1);
|
|
243
|
+
logger.info(`Using specified project type: ${projectLabel}`);
|
|
244
|
+
} else {
|
|
245
|
+
const detected = detectProject();
|
|
246
|
+
projectType = detected.type;
|
|
247
|
+
projectLabel = detected.label;
|
|
248
|
+
logger.info(`Detected project type: ${projectLabel}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Get rules
|
|
252
|
+
const rules = opts.all ? getAllRules() : getRulesForProject(projectType);
|
|
253
|
+
logger.info(`Using ${rules.length} rule(s) for checklist.`);
|
|
254
|
+
|
|
255
|
+
// Generate the checklist
|
|
256
|
+
const checklist = generateChecklist(rules, projectLabel);
|
|
257
|
+
|
|
258
|
+
// Output
|
|
259
|
+
if (opts.output) {
|
|
260
|
+
const outputPath = path.resolve(opts.output);
|
|
261
|
+
fs.writeFileSync(outputPath, checklist, 'utf-8');
|
|
262
|
+
logger.success(`PR checklist written to: ${outputPath}`);
|
|
263
|
+
} else {
|
|
264
|
+
// Print to stdout
|
|
265
|
+
console.log('\n' + checklist);
|
|
266
|
+
logger.success('PR checklist generated. Copy the above into your PR description!');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Also copy to clipboard if pbcopy/xclip is available
|
|
270
|
+
try {
|
|
271
|
+
if (process.platform === 'darwin') {
|
|
272
|
+
execSync('pbcopy', { input: checklist });
|
|
273
|
+
logger.dim('📋 Copied to clipboard (macOS)');
|
|
274
|
+
} else if (process.platform === 'linux') {
|
|
275
|
+
execSync('xclip -selection clipboard', { input: checklist });
|
|
276
|
+
logger.dim('📋 Copied to clipboard (Linux)');
|
|
277
|
+
}
|
|
278
|
+
} catch {
|
|
279
|
+
// Clipboard copy is a nice-to-have, not critical
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── Execute ─────────────────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
main();
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* precommit-check.ts — Main pre-commit hook script
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* Entrypoint for the `npm run precommit-check` command.
|
|
7
|
+
* This script:
|
|
8
|
+
* 1. Checks bypass conditions (commit message, env variable)
|
|
9
|
+
* 2. Detects the project type (Angular / React / NextJS)
|
|
10
|
+
* 3. Loads the appropriate rule configuration
|
|
11
|
+
* 4. Reads all staged files
|
|
12
|
+
* 5. Executes the rule engine
|
|
13
|
+
* 6. Runs ESLint and Prettier on staged files
|
|
14
|
+
* 7. Reports results and exits with code 1 if errors are found
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* npm run precommit-check
|
|
18
|
+
* BYPASS_RULES=true npm run precommit-check # skip all checks
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { execSync } from 'child_process';
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
|
|
24
|
+
// ── Import utilities ────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
import { detectProject, ProjectType } from './utils/project-detector';
|
|
27
|
+
import { getStagedFiles, getCommitMessage, readStagedFiles } from './utils/file-checker';
|
|
28
|
+
import { getRulesForProject } from '../config/guidelines.config';
|
|
29
|
+
import { executeRules, printReport, RuleEngineReport } from './utils/rule-engine';
|
|
30
|
+
import * as logger from './utils/logger';
|
|
31
|
+
import { verifyBypassPassword, recordBypass } from './utils/bypass-manager';
|
|
32
|
+
import * as readline from 'readline';
|
|
33
|
+
|
|
34
|
+
// ── Load all project-type configs (side-effect: registers rules) ────────────
|
|
35
|
+
|
|
36
|
+
import '../config/angular.config';
|
|
37
|
+
import '../config/react.config';
|
|
38
|
+
import '../config/nextjs.config';
|
|
39
|
+
|
|
40
|
+
// ── Constants ───────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
/** The bypass flag that can appear in a commit message */
|
|
43
|
+
const BYPASS_COMMIT_FLAG = '#bypass-rules';
|
|
44
|
+
|
|
45
|
+
/** Environment variable to bypass all checks */
|
|
46
|
+
const BYPASS_ENV_VAR = 'BYPASS_RULES';
|
|
47
|
+
|
|
48
|
+
/** File extensions we care about for linting */
|
|
49
|
+
const LINTABLE_EXTENSIONS = ['ts', 'tsx', 'js', 'jsx', 'html', 'css', 'scss'];
|
|
50
|
+
|
|
51
|
+
/** File extensions for code guardian rule checks */
|
|
52
|
+
const CHECKABLE_EXTENSIONS = ['ts', 'tsx', 'js', 'jsx', 'html', 'css', 'scss', 'sass', 'less'];
|
|
53
|
+
|
|
54
|
+
// ── Bypass detection ────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Prompt user for input (password or reason)
|
|
58
|
+
*/
|
|
59
|
+
function promptUser(question: string, hidden: boolean = false): Promise<string> {
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
const rl = readline.createInterface({
|
|
62
|
+
input: process.stdin,
|
|
63
|
+
output: process.stdout,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (hidden) {
|
|
67
|
+
// Hide password input
|
|
68
|
+
const stdin = process.stdin as any;
|
|
69
|
+
stdin.setRawMode(true);
|
|
70
|
+
|
|
71
|
+
let password = '';
|
|
72
|
+
process.stdout.write(question);
|
|
73
|
+
|
|
74
|
+
stdin.on('data', (char: Buffer) => {
|
|
75
|
+
const str = char.toString('utf-8');
|
|
76
|
+
|
|
77
|
+
if (str === '\n' || str === '\r' || str === '\u0004') {
|
|
78
|
+
stdin.setRawMode(false);
|
|
79
|
+
process.stdout.write('\n');
|
|
80
|
+
rl.close();
|
|
81
|
+
resolve(password);
|
|
82
|
+
} else if (str === '\u0003') {
|
|
83
|
+
// Ctrl+C
|
|
84
|
+
process.exit(1);
|
|
85
|
+
} else if (str === '\u007f' || str === '\b') {
|
|
86
|
+
// Backspace
|
|
87
|
+
if (password.length > 0) {
|
|
88
|
+
password = password.slice(0, -1);
|
|
89
|
+
process.stdout.write('\b \b');
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
password += str;
|
|
93
|
+
process.stdout.write('*');
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
} else {
|
|
97
|
+
rl.question(question, (answer) => {
|
|
98
|
+
rl.close();
|
|
99
|
+
resolve(answer);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if the pre-commit checks should be bypassed.
|
|
107
|
+
* Three methods:
|
|
108
|
+
* 1. Environment variable `BYPASS_RULES=true` (no password required)
|
|
109
|
+
* 2. Commit message contains `#bypass-rules` (requires password)
|
|
110
|
+
* 3. Interactive password prompt (if errors found)
|
|
111
|
+
*/
|
|
112
|
+
async function shouldBypass(commitMessage: string): Promise<{ bypassed: boolean; reason: string; method: 'commit-message' | 'env-variable' | 'password' }> {
|
|
113
|
+
// Check environment variable (for CI/CD - no password required)
|
|
114
|
+
if (process.env[BYPASS_ENV_VAR]?.toLowerCase() === 'true') {
|
|
115
|
+
// Allow custom reason via BYPASS_REASON environment variable
|
|
116
|
+
const customReason = process.env['BYPASS_REASON'];
|
|
117
|
+
const reason = customReason || 'Automated bypass via environment variable';
|
|
118
|
+
recordBypass(reason, commitMessage, 'env-variable');
|
|
119
|
+
return { bypassed: true, reason: `Environment variable ${BYPASS_ENV_VAR}=true`, method: 'env-variable' };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check commit message flag (requires password)
|
|
123
|
+
if (commitMessage.includes(BYPASS_COMMIT_FLAG)) {
|
|
124
|
+
logger.warn(`Bypass flag detected in commit message: "${BYPASS_COMMIT_FLAG}"`);
|
|
125
|
+
logger.info('Password required to bypass checks.');
|
|
126
|
+
|
|
127
|
+
const password = await promptUser('🔐 Enter bypass password: ', true);
|
|
128
|
+
|
|
129
|
+
if (verifyBypassPassword(password)) {
|
|
130
|
+
const reason = await promptUser('📝 Reason for bypass: ', false);
|
|
131
|
+
recordBypass(reason || 'No reason provided', commitMessage, 'commit-message');
|
|
132
|
+
return { bypassed: true, reason: `Commit message contains "${BYPASS_COMMIT_FLAG}" (authenticated)`, method: 'commit-message' };
|
|
133
|
+
} else {
|
|
134
|
+
logger.error('❌ Invalid password. Bypass denied.');
|
|
135
|
+
return { bypassed: false, reason: '', method: 'commit-message' };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { bypassed: false, reason: '', method: 'commit-message' };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── ESLint runner ───────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Run ESLint on staged files. Returns true if linting passed.
|
|
146
|
+
*/
|
|
147
|
+
function runEslint(stagedFiles: string[]): boolean {
|
|
148
|
+
const lintableFiles = stagedFiles.filter((f) => {
|
|
149
|
+
const ext = path.extname(f).replace(/^\./, '');
|
|
150
|
+
return LINTABLE_EXTENSIONS.includes(ext);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (lintableFiles.length === 0) {
|
|
154
|
+
logger.dim('No lintable files found — skipping ESLint.');
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
logger.info(`Running ESLint on ${lintableFiles.length} file(s)...`);
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const fileList = lintableFiles.join(' ');
|
|
162
|
+
execSync(`npx eslint ${fileList} --max-warnings 0`, {
|
|
163
|
+
stdio: 'inherit',
|
|
164
|
+
encoding: 'utf-8',
|
|
165
|
+
});
|
|
166
|
+
logger.success('ESLint passed.');
|
|
167
|
+
return true;
|
|
168
|
+
} catch {
|
|
169
|
+
logger.error('ESLint found issues. Fix them before committing.');
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── Prettier runner ─────────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Run Prettier on staged files in check mode. Returns true if formatting is OK.
|
|
178
|
+
* If formatting issues are found, runs Prettier --write and re-stages the files.
|
|
179
|
+
*/
|
|
180
|
+
function runPrettier(stagedFiles: string[]): boolean {
|
|
181
|
+
const formattableFiles = stagedFiles.filter((f) => {
|
|
182
|
+
const ext = path.extname(f).replace(/^\./, '');
|
|
183
|
+
return LINTABLE_EXTENSIONS.includes(ext);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (formattableFiles.length === 0) {
|
|
187
|
+
logger.dim('No formattable files found — skipping Prettier.');
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
logger.info(`Running Prettier on ${formattableFiles.length} file(s)...`);
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const fileList = formattableFiles.join(' ');
|
|
195
|
+
execSync(`npx prettier --check ${fileList}`, {
|
|
196
|
+
stdio: 'pipe',
|
|
197
|
+
encoding: 'utf-8',
|
|
198
|
+
});
|
|
199
|
+
logger.success('Prettier check passed.');
|
|
200
|
+
return true;
|
|
201
|
+
} catch {
|
|
202
|
+
logger.warn('Prettier found formatting issues — auto-fixing...');
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const fileList = formattableFiles.join(' ');
|
|
206
|
+
execSync(`npx prettier --write ${fileList}`, {
|
|
207
|
+
stdio: 'inherit',
|
|
208
|
+
encoding: 'utf-8',
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Re-stage the auto-formatted files
|
|
212
|
+
execSync(`git add ${formattableFiles.join(' ')}`, {
|
|
213
|
+
stdio: 'inherit',
|
|
214
|
+
encoding: 'utf-8',
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
logger.success('Prettier auto-fixed and re-staged files.');
|
|
218
|
+
return true;
|
|
219
|
+
} catch {
|
|
220
|
+
logger.error('Prettier auto-fix failed.');
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ── Feature doc detection ───────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* If the commit looks like a new feature (branch name contains "feat/" or
|
|
230
|
+
* "feature/"), remind the user to generate docs.
|
|
231
|
+
*/
|
|
232
|
+
function checkFeatureDocsReminder(): void {
|
|
233
|
+
try {
|
|
234
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim();
|
|
235
|
+
|
|
236
|
+
if (branch.startsWith('feat/') || branch.startsWith('feature/')) {
|
|
237
|
+
const featureName = branch.replace(/^(feat|feature)\//, '');
|
|
238
|
+
const docPath = path.join('docs', 'features', `${featureName}.md`);
|
|
239
|
+
|
|
240
|
+
const fs = require('fs');
|
|
241
|
+
if (!fs.existsSync(docPath)) {
|
|
242
|
+
logger.warn(
|
|
243
|
+
`Feature branch detected: "${branch}"\n` +
|
|
244
|
+
` 📄 No feature doc found at ${docPath}\n` +
|
|
245
|
+
` 💡 Run: npm run generate-doc -- --name="${featureName}" --type=ui`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} catch {
|
|
250
|
+
// Silently ignore — not critical
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── Main ────────────────────────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
async function main(): Promise<void> {
|
|
257
|
+
logger.header('🛡️ Code Guardian — Pre-commit Check');
|
|
258
|
+
|
|
259
|
+
// Get commit message early
|
|
260
|
+
const commitMessage = getCommitMessage();
|
|
261
|
+
|
|
262
|
+
// Step 1: Check for bypass
|
|
263
|
+
const bypass = await shouldBypass(commitMessage);
|
|
264
|
+
if (bypass.bypassed) {
|
|
265
|
+
logger.warn(`Checks BYPASSED: ${bypass.reason}`);
|
|
266
|
+
logger.warn('⚠️ All rules skipped. Make sure this is intentional!');
|
|
267
|
+
process.exit(0);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Step 2: Detect project type
|
|
271
|
+
const project = detectProject();
|
|
272
|
+
logger.info(`Detected project: ${project.label}`);
|
|
273
|
+
|
|
274
|
+
if (project.type === 'unknown') {
|
|
275
|
+
logger.warn(
|
|
276
|
+
'Could not detect project type. Only shared rules will be applied.\n' +
|
|
277
|
+
' Hint: Make sure package.json has @angular/core, react, or next as a dependency.'
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Step 3: Get staged files
|
|
282
|
+
const stagedPaths = getStagedFiles();
|
|
283
|
+
if (stagedPaths.length === 0) {
|
|
284
|
+
logger.info('No staged files found — nothing to check.');
|
|
285
|
+
process.exit(0);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
logger.info(`Found ${stagedPaths.length} staged file(s).`);
|
|
289
|
+
|
|
290
|
+
// Step 4: Read file contents
|
|
291
|
+
const files = readStagedFiles(CHECKABLE_EXTENSIONS);
|
|
292
|
+
if (files.length === 0) {
|
|
293
|
+
logger.info('No checkable files in staging — skipping rule engine.');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Step 5: Load rules and execute
|
|
297
|
+
let report: RuleEngineReport = {
|
|
298
|
+
violations: [],
|
|
299
|
+
errorCount: 0,
|
|
300
|
+
warningCount: 0,
|
|
301
|
+
infoCount: 0,
|
|
302
|
+
filesChecked: 0,
|
|
303
|
+
rulesExecuted: 0,
|
|
304
|
+
bypassed: false,
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
if (files.length > 0) {
|
|
308
|
+
const rules = getRulesForProject(project.type);
|
|
309
|
+
logger.info(`Loaded ${rules.length} rule(s) for ${project.label}.`);
|
|
310
|
+
|
|
311
|
+
report = executeRules(files, rules, true);
|
|
312
|
+
printReport(report);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Step 6: Run ESLint
|
|
316
|
+
const eslintPassed = runEslint(stagedPaths);
|
|
317
|
+
|
|
318
|
+
// Step 7: Run Prettier (auto-fix mode)
|
|
319
|
+
const prettierPassed = runPrettier(stagedPaths);
|
|
320
|
+
|
|
321
|
+
// Step 8: Check for feature docs
|
|
322
|
+
checkFeatureDocsReminder();
|
|
323
|
+
|
|
324
|
+
// Step 9: Final verdict
|
|
325
|
+
console.log('');
|
|
326
|
+
if (report.errorCount > 0 || !eslintPassed) {
|
|
327
|
+
logger.error('❌ Commit BLOCKED — fix the errors above and try again.');
|
|
328
|
+
logger.dim(` To bypass: use "${BYPASS_COMMIT_FLAG}" in commit message or set ${BYPASS_ENV_VAR}=true`);
|
|
329
|
+
|
|
330
|
+
// Exit immediately with error code - no interactive bypass in hook
|
|
331
|
+
// User must use #bypass-rules in commit message to bypass
|
|
332
|
+
process.exitCode = 1;
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (report.warningCount > 0 || !prettierPassed) {
|
|
337
|
+
logger.warn('⚠️ Commit allowed with warnings. Please address them soon.');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
logger.success('✅ All checks passed — commit allowed!');
|
|
341
|
+
process.exit(0);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ── Execute ─────────────────────────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
main().catch((err) => {
|
|
347
|
+
logger.error(`Unexpected error: ${err.message || err}`);
|
|
348
|
+
process.exit(1);
|
|
349
|
+
});
|