@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,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* code-analyzer.ts — Automatic documentation from TypeScript/React code
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* Analyzes TypeScript/React components and extracts:
|
|
7
|
+
* - Component props and their types
|
|
8
|
+
* - JSDoc comments
|
|
9
|
+
* - Default values
|
|
10
|
+
* - Event handlers
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
import { Project, SourceFile, InterfaceDeclaration, TypeAliasDeclaration, PropertySignature } from 'ts-morph';
|
|
16
|
+
|
|
17
|
+
export interface PropInfo {
|
|
18
|
+
name: string;
|
|
19
|
+
type: string;
|
|
20
|
+
required: boolean;
|
|
21
|
+
defaultValue?: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ComponentInfo {
|
|
26
|
+
name: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
props: PropInfo[];
|
|
29
|
+
filePath: string;
|
|
30
|
+
isReactComponent: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Analyze a TypeScript/React component file
|
|
35
|
+
*/
|
|
36
|
+
export function analyzeComponent(filePath: string): ComponentInfo | null {
|
|
37
|
+
if (!fs.existsSync(filePath)) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const project = new Project({
|
|
42
|
+
compilerOptions: {
|
|
43
|
+
jsx: 1, // React JSX
|
|
44
|
+
target: 99, // ESNext
|
|
45
|
+
module: 99, // ESNext
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const sourceFile = project.addSourceFileAtPath(filePath);
|
|
50
|
+
const fileName = path.basename(filePath, path.extname(filePath));
|
|
51
|
+
|
|
52
|
+
// Try to find component name and props
|
|
53
|
+
const componentInfo = extractComponentInfo(sourceFile, fileName);
|
|
54
|
+
|
|
55
|
+
if (!componentInfo) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
name: componentInfo.name,
|
|
61
|
+
description: componentInfo.description,
|
|
62
|
+
props: componentInfo.props,
|
|
63
|
+
filePath,
|
|
64
|
+
isReactComponent: componentInfo.isReactComponent,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Extract component information from source file
|
|
70
|
+
*/
|
|
71
|
+
function extractComponentInfo(sourceFile: SourceFile, defaultName: string): ComponentInfo | null {
|
|
72
|
+
let componentName = toPascalCase(defaultName);
|
|
73
|
+
let props: PropInfo[] = [];
|
|
74
|
+
let description: string | undefined;
|
|
75
|
+
let isReactComponent = false;
|
|
76
|
+
|
|
77
|
+
// Look for React component (function or arrow function)
|
|
78
|
+
const functionDeclarations = sourceFile.getFunctions();
|
|
79
|
+
const variableStatements = sourceFile.getVariableStatements();
|
|
80
|
+
|
|
81
|
+
// Check function declarations
|
|
82
|
+
for (const func of functionDeclarations) {
|
|
83
|
+
const name = func.getName();
|
|
84
|
+
if (name && /^[A-Z]/.test(name)) {
|
|
85
|
+
componentName = name;
|
|
86
|
+
isReactComponent = true;
|
|
87
|
+
description = extractJsDocComment(func);
|
|
88
|
+
|
|
89
|
+
// Extract props from parameters
|
|
90
|
+
const params = func.getParameters();
|
|
91
|
+
if (params.length > 0) {
|
|
92
|
+
const propsParam = params[0];
|
|
93
|
+
const typeNode = propsParam.getTypeNode();
|
|
94
|
+
if (typeNode) {
|
|
95
|
+
props = extractPropsFromType(sourceFile, typeNode.getText());
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check variable declarations (arrow functions)
|
|
103
|
+
if (!isReactComponent) {
|
|
104
|
+
for (const statement of variableStatements) {
|
|
105
|
+
const declarations = statement.getDeclarations();
|
|
106
|
+
for (const decl of declarations) {
|
|
107
|
+
const name = decl.getName();
|
|
108
|
+
if (/^[A-Z]/.test(name)) {
|
|
109
|
+
const initializer = decl.getInitializer();
|
|
110
|
+
if (initializer && (initializer.getKindName() === 'ArrowFunction' || initializer.getKindName() === 'FunctionExpression')) {
|
|
111
|
+
componentName = name;
|
|
112
|
+
isReactComponent = true;
|
|
113
|
+
description = extractJsDocComment(decl);
|
|
114
|
+
|
|
115
|
+
// Extract props from arrow function parameters
|
|
116
|
+
const arrowFunc = initializer.asKind(218); // ArrowFunction kind
|
|
117
|
+
if (arrowFunc) {
|
|
118
|
+
const params = arrowFunc.getParameters();
|
|
119
|
+
if (params.length > 0) {
|
|
120
|
+
const propsParam = params[0];
|
|
121
|
+
const typeNode = propsParam.getTypeNode();
|
|
122
|
+
if (typeNode) {
|
|
123
|
+
props = extractPropsFromType(sourceFile, typeNode.getText());
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (isReactComponent) break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// If still not found, look for props interface/type
|
|
136
|
+
if (props.length === 0) {
|
|
137
|
+
props = findPropsInterface(sourceFile, componentName);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
name: componentName,
|
|
142
|
+
description,
|
|
143
|
+
props,
|
|
144
|
+
isReactComponent,
|
|
145
|
+
filePath: sourceFile.getFilePath(),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Find props interface or type alias
|
|
151
|
+
*/
|
|
152
|
+
function findPropsInterface(sourceFile: SourceFile, componentName: string): PropInfo[] {
|
|
153
|
+
const possibleNames = [
|
|
154
|
+
`${componentName}Props`,
|
|
155
|
+
`I${componentName}Props`,
|
|
156
|
+
`${componentName}Properties`,
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
// Check interfaces
|
|
160
|
+
for (const interfaceDecl of sourceFile.getInterfaces()) {
|
|
161
|
+
const name = interfaceDecl.getName();
|
|
162
|
+
if (possibleNames.includes(name) || name.toLowerCase().includes('props')) {
|
|
163
|
+
return extractPropsFromInterface(interfaceDecl);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Check type aliases
|
|
168
|
+
for (const typeAlias of sourceFile.getTypeAliases()) {
|
|
169
|
+
const name = typeAlias.getName();
|
|
170
|
+
if (possibleNames.includes(name) || name.toLowerCase().includes('props')) {
|
|
171
|
+
return extractPropsFromTypeAlias(typeAlias);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Extract props from interface declaration
|
|
180
|
+
*/
|
|
181
|
+
function extractPropsFromInterface(interfaceDecl: InterfaceDeclaration): PropInfo[] {
|
|
182
|
+
const props: PropInfo[] = [];
|
|
183
|
+
|
|
184
|
+
for (const prop of interfaceDecl.getProperties()) {
|
|
185
|
+
props.push(extractPropInfo(prop));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return props;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Extract props from type alias
|
|
193
|
+
*/
|
|
194
|
+
function extractPropsFromTypeAlias(typeAlias: TypeAliasDeclaration): PropInfo[] {
|
|
195
|
+
const props: PropInfo[] = [];
|
|
196
|
+
const typeNode = typeAlias.getTypeNode();
|
|
197
|
+
|
|
198
|
+
if (typeNode && typeNode.getKindName() === 'TypeLiteral') {
|
|
199
|
+
const members = (typeNode as any).getMembers();
|
|
200
|
+
for (const member of members) {
|
|
201
|
+
if (member.getKindName() === 'PropertySignature') {
|
|
202
|
+
props.push(extractPropInfo(member));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return props;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Extract props from type reference string
|
|
212
|
+
*/
|
|
213
|
+
function extractPropsFromType(sourceFile: SourceFile, typeName: string): PropInfo[] {
|
|
214
|
+
// Remove generic brackets and get base type name
|
|
215
|
+
const baseType = typeName.replace(/<.*>/, '').trim();
|
|
216
|
+
|
|
217
|
+
// Find the interface or type
|
|
218
|
+
const interfaceDecl = sourceFile.getInterface(baseType);
|
|
219
|
+
if (interfaceDecl) {
|
|
220
|
+
return extractPropsFromInterface(interfaceDecl);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const typeAlias = sourceFile.getTypeAlias(baseType);
|
|
224
|
+
if (typeAlias) {
|
|
225
|
+
return extractPropsFromTypeAlias(typeAlias);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Extract individual prop information
|
|
233
|
+
*/
|
|
234
|
+
function extractPropInfo(prop: PropertySignature): PropInfo {
|
|
235
|
+
const name = prop.getName();
|
|
236
|
+
const type = prop.getType().getText();
|
|
237
|
+
const required = !prop.hasQuestionToken();
|
|
238
|
+
const description = extractJsDocComment(prop);
|
|
239
|
+
|
|
240
|
+
// Try to get default value
|
|
241
|
+
let defaultValue: string | undefined;
|
|
242
|
+
const initializer = prop.getInitializer();
|
|
243
|
+
if (initializer) {
|
|
244
|
+
defaultValue = initializer.getText();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
name,
|
|
249
|
+
type: simplifyType(type),
|
|
250
|
+
required,
|
|
251
|
+
defaultValue,
|
|
252
|
+
description,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Extract JSDoc comment
|
|
258
|
+
*/
|
|
259
|
+
function extractJsDocComment(node: any): string | undefined {
|
|
260
|
+
try {
|
|
261
|
+
if (typeof node.getJsDocs !== 'function') {
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
const jsDocs = node.getJsDocs();
|
|
265
|
+
if (jsDocs && jsDocs.length > 0) {
|
|
266
|
+
const comment = jsDocs[0].getDescription().trim();
|
|
267
|
+
return comment || undefined;
|
|
268
|
+
}
|
|
269
|
+
} catch (err) {
|
|
270
|
+
// Silently fail
|
|
271
|
+
}
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Simplify complex type names for documentation
|
|
277
|
+
*/
|
|
278
|
+
function simplifyType(type: string): string {
|
|
279
|
+
// Remove 'import("...")' wrapper
|
|
280
|
+
type = type.replace(/import\([^)]+\)\./g, '');
|
|
281
|
+
|
|
282
|
+
// Simplify React types
|
|
283
|
+
type = type.replace(/React\./g, '');
|
|
284
|
+
type = type.replace(/MouseEvent<.*?>/g, 'MouseEvent');
|
|
285
|
+
type = type.replace(/ChangeEvent<.*?>/g, 'ChangeEvent');
|
|
286
|
+
|
|
287
|
+
// Shorten long union types
|
|
288
|
+
if (type.length > 50 && type.includes('|')) {
|
|
289
|
+
const parts = type.split('|');
|
|
290
|
+
if (parts.length > 3) {
|
|
291
|
+
return `${parts.slice(0, 3).join(' | ')} | ...`;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return type;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Convert kebab-case or snake_case to PascalCase
|
|
300
|
+
*/
|
|
301
|
+
function toPascalCase(str: string): string {
|
|
302
|
+
return str
|
|
303
|
+
.split(/[-_]/)
|
|
304
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
305
|
+
.join('');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Search for component files in directory
|
|
310
|
+
*/
|
|
311
|
+
export function findComponentFile(componentName: string, searchDir: string = process.cwd()): string | null {
|
|
312
|
+
const possibleNames = [
|
|
313
|
+
componentName,
|
|
314
|
+
componentName.toLowerCase(),
|
|
315
|
+
componentName.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, ''),
|
|
316
|
+
];
|
|
317
|
+
|
|
318
|
+
const possibleExtensions = ['.tsx', '.ts', '.jsx', '.js'];
|
|
319
|
+
const possibleDirs = [
|
|
320
|
+
'src/components',
|
|
321
|
+
'components',
|
|
322
|
+
'src',
|
|
323
|
+
'.',
|
|
324
|
+
];
|
|
325
|
+
|
|
326
|
+
for (const dir of possibleDirs) {
|
|
327
|
+
const fullDir = path.join(searchDir, dir);
|
|
328
|
+
if (!fs.existsSync(fullDir)) continue;
|
|
329
|
+
|
|
330
|
+
for (const name of possibleNames) {
|
|
331
|
+
for (const ext of possibleExtensions) {
|
|
332
|
+
const filePath = path.join(fullDir, name + ext);
|
|
333
|
+
if (fs.existsSync(filePath)) {
|
|
334
|
+
return filePath;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* file-checker.ts — Low-level file analysis utilities
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* Provides helpers for reading staged files, counting lines, extracting
|
|
7
|
+
* template content, and running regex-based checks against source code.
|
|
8
|
+
* Used by the rule engine to apply individual lint rules.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { execSync } from 'child_process';
|
|
14
|
+
|
|
15
|
+
// ── Types ───────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export interface FileInfo {
|
|
18
|
+
/** Absolute path to the file */
|
|
19
|
+
absolutePath: string;
|
|
20
|
+
/** Path relative to repo root */
|
|
21
|
+
relativePath: string;
|
|
22
|
+
/** File extension without the leading dot (e.g. "ts", "tsx", "html") */
|
|
23
|
+
extension: string;
|
|
24
|
+
/** Full file content (UTF-8) */
|
|
25
|
+
content: string;
|
|
26
|
+
/** Individual lines (1-indexed access via lines[lineNumber - 1]) */
|
|
27
|
+
lines: string[];
|
|
28
|
+
/** Total line count */
|
|
29
|
+
lineCount: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CheckMatch {
|
|
33
|
+
/** 1-based line number where the match was found */
|
|
34
|
+
line: number;
|
|
35
|
+
/** The content of the matched line (trimmed) */
|
|
36
|
+
text: string;
|
|
37
|
+
/** Column offset (0-based) within the line */
|
|
38
|
+
column: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Git helpers ─────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns the list of staged files (relative paths) using `git diff --cached`.
|
|
45
|
+
* Filters out deleted files (status "D") so we only check files that exist.
|
|
46
|
+
*/
|
|
47
|
+
export function getStagedFiles(): string[] {
|
|
48
|
+
try {
|
|
49
|
+
const output = execSync('git diff --cached --name-only --diff-filter=d', {
|
|
50
|
+
encoding: 'utf-8',
|
|
51
|
+
}).trim();
|
|
52
|
+
|
|
53
|
+
if (!output) return [];
|
|
54
|
+
return output.split('\n').filter(Boolean);
|
|
55
|
+
} catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Returns the current commit message from `.git/COMMIT_EDITMSG`.
|
|
62
|
+
* Falls back to empty string if unavailable.
|
|
63
|
+
*/
|
|
64
|
+
export function getCommitMessage(): string {
|
|
65
|
+
try {
|
|
66
|
+
// During a pre-commit hook the message file may not exist yet.
|
|
67
|
+
// We also support reading from the HUSKY_GIT_PARAMS env.
|
|
68
|
+
const msgFile = path.join(process.cwd(), '.git', 'COMMIT_EDITMSG');
|
|
69
|
+
if (fs.existsSync(msgFile)) {
|
|
70
|
+
return fs.readFileSync(msgFile, 'utf-8').trim();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Fallback: try getting the last commit message (for post-commit uses)
|
|
74
|
+
return execSync('git log -1 --pretty=%B 2>/dev/null', { encoding: 'utf-8' }).trim();
|
|
75
|
+
} catch {
|
|
76
|
+
return '';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Returns the current branch name.
|
|
82
|
+
*/
|
|
83
|
+
export function getCurrentBranch(): string {
|
|
84
|
+
try {
|
|
85
|
+
return execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim();
|
|
86
|
+
} catch {
|
|
87
|
+
return 'unknown';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── File reading ────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Read a single file and return a structured `FileInfo` object.
|
|
95
|
+
*/
|
|
96
|
+
export function readFileInfo(relativePath: string): FileInfo | null {
|
|
97
|
+
const absolutePath = path.resolve(process.cwd(), relativePath);
|
|
98
|
+
|
|
99
|
+
if (!fs.existsSync(absolutePath)) return null;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const content = fs.readFileSync(absolutePath, 'utf-8');
|
|
103
|
+
const lines = content.split('\n');
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
absolutePath,
|
|
107
|
+
relativePath,
|
|
108
|
+
extension: path.extname(relativePath).replace(/^\./, ''),
|
|
109
|
+
content,
|
|
110
|
+
lines,
|
|
111
|
+
lineCount: lines.length,
|
|
112
|
+
};
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Read multiple staged files, filtering by allowed extensions.
|
|
120
|
+
*/
|
|
121
|
+
export function readStagedFiles(extensions?: string[]): FileInfo[] {
|
|
122
|
+
const staged = getStagedFiles();
|
|
123
|
+
const files: FileInfo[] = [];
|
|
124
|
+
|
|
125
|
+
for (const rel of staged) {
|
|
126
|
+
const ext = path.extname(rel).replace(/^\./, '');
|
|
127
|
+
|
|
128
|
+
// Skip files that don't match the allowed extensions
|
|
129
|
+
if (extensions && extensions.length > 0 && !extensions.includes(ext)) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const info = readFileInfo(rel);
|
|
134
|
+
if (info) files.push(info);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return files;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── Content checks ──────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Search a file's content for all matches of the given regex.
|
|
144
|
+
* Returns an array of matches with line numbers and text.
|
|
145
|
+
*/
|
|
146
|
+
export function findMatches(file: FileInfo, pattern: RegExp): CheckMatch[] {
|
|
147
|
+
const matches: CheckMatch[] = [];
|
|
148
|
+
|
|
149
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
150
|
+
const line = file.lines[i];
|
|
151
|
+
|
|
152
|
+
// Skip lines that are comments
|
|
153
|
+
const trimmed = line.trim();
|
|
154
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Reset lastIndex for global regexps
|
|
159
|
+
pattern.lastIndex = 0;
|
|
160
|
+
|
|
161
|
+
const match = pattern.exec(line);
|
|
162
|
+
if (match) {
|
|
163
|
+
matches.push({
|
|
164
|
+
line: i + 1, // 1-based
|
|
165
|
+
text: trimmed,
|
|
166
|
+
column: match.index,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return matches;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Check if a file exceeds the maximum allowed line count.
|
|
176
|
+
*/
|
|
177
|
+
export function exceedsMaxLines(file: FileInfo, maxLines: number): boolean {
|
|
178
|
+
return file.lineCount > maxLines;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Extract Angular inline template content from a component file.
|
|
183
|
+
* Looks for `template: \`...\`` blocks.
|
|
184
|
+
*/
|
|
185
|
+
export function extractInlineTemplate(file: FileInfo): { content: string; startLine: number } | null {
|
|
186
|
+
const templateRegex = /template\s*:\s*`([\s\S]*?)`/;
|
|
187
|
+
const match = templateRegex.exec(file.content);
|
|
188
|
+
|
|
189
|
+
if (!match) return null;
|
|
190
|
+
|
|
191
|
+
// Find the line number where the template starts
|
|
192
|
+
const before = file.content.substring(0, match.index);
|
|
193
|
+
const startLine = before.split('\n').length;
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
content: match[1],
|
|
197
|
+
startLine,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Extract content from Angular HTML template files.
|
|
203
|
+
* Returns the full file content for .html files associated with Angular components.
|
|
204
|
+
*/
|
|
205
|
+
export function extractExternalTemplate(file: FileInfo): string | null {
|
|
206
|
+
if (file.extension !== 'html') return null;
|
|
207
|
+
return file.content;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Check if a file has a specific inline suppression comment.
|
|
212
|
+
* Supports: // code-guardian-disable <rule-id>
|
|
213
|
+
*/
|
|
214
|
+
export function hasInlineSuppression(file: FileInfo, ruleId: string, lineNumber: number): boolean {
|
|
215
|
+
// Check the line itself
|
|
216
|
+
if (file.lines[lineNumber - 1]?.includes(`code-guardian-disable ${ruleId}`)) {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check the line above
|
|
221
|
+
if (lineNumber > 1 && file.lines[lineNumber - 2]?.includes(`code-guardian-disable ${ruleId}`)) {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Check file-level suppression at the top of the file
|
|
226
|
+
const firstLines = file.lines.slice(0, 5).join('\n');
|
|
227
|
+
if (firstLines.includes(`code-guardian-disable-file ${ruleId}`)) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Detect whether a file contains JSX/TSX content (React templates).
|
|
236
|
+
*/
|
|
237
|
+
export function isJsxFile(file: FileInfo): boolean {
|
|
238
|
+
return ['jsx', 'tsx'].includes(file.extension);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Detect whether a file is an Angular component.
|
|
243
|
+
*/
|
|
244
|
+
export function isAngularComponent(file: FileInfo): boolean {
|
|
245
|
+
return file.content.includes('@Component') && ['ts'].includes(file.extension);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Detect whether a file is a style file.
|
|
250
|
+
*/
|
|
251
|
+
export function isStyleFile(file: FileInfo): boolean {
|
|
252
|
+
return ['css', 'scss', 'sass', 'less'].includes(file.extension);
|
|
253
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* logger.ts — Colored, structured console output for the Code Guardian toolkit
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* Provides severity-based logging with ANSI color codes.
|
|
7
|
+
* No external dependency required (chalk is optional — we use raw ANSI here
|
|
8
|
+
* for zero-dep portability).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ── ANSI escape helpers ─────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
const RESET = '\x1b[0m';
|
|
14
|
+
const BOLD = '\x1b[1m';
|
|
15
|
+
const DIM = '\x1b[2m';
|
|
16
|
+
|
|
17
|
+
const FG = {
|
|
18
|
+
red: '\x1b[31m',
|
|
19
|
+
green: '\x1b[32m',
|
|
20
|
+
yellow: '\x1b[33m',
|
|
21
|
+
blue: '\x1b[34m',
|
|
22
|
+
magenta: '\x1b[35m',
|
|
23
|
+
cyan: '\x1b[36m',
|
|
24
|
+
white: '\x1b[37m',
|
|
25
|
+
gray: '\x1b[90m',
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
const BG = {
|
|
29
|
+
red: '\x1b[41m',
|
|
30
|
+
green: '\x1b[42m',
|
|
31
|
+
yellow: '\x1b[43m',
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/** Informational message — cyan prefix */
|
|
37
|
+
export function info(msg: string): void {
|
|
38
|
+
console.log(`${FG.cyan}ℹ ${BOLD}[INFO]${RESET}${FG.cyan} ${msg}${RESET}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Success message — green prefix */
|
|
42
|
+
export function success(msg: string): void {
|
|
43
|
+
console.log(`${FG.green}✔ ${BOLD}[PASS]${RESET}${FG.green} ${msg}${RESET}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Warning message — yellow prefix */
|
|
47
|
+
export function warn(msg: string): void {
|
|
48
|
+
console.log(`${FG.yellow}⚠ ${BOLD}[WARN]${RESET}${FG.yellow} ${msg}${RESET}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Error message — red prefix */
|
|
52
|
+
export function error(msg: string): void {
|
|
53
|
+
console.error(`${FG.red}✖ ${BOLD}[FAIL]${RESET}${FG.red} ${msg}${RESET}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Section header — bold magenta divider */
|
|
57
|
+
export function header(title: string): void {
|
|
58
|
+
const line = '─'.repeat(60);
|
|
59
|
+
console.log(`\n${FG.magenta}${BOLD}${line}${RESET}`);
|
|
60
|
+
console.log(`${FG.magenta}${BOLD} ${title}${RESET}`);
|
|
61
|
+
console.log(`${FG.magenta}${BOLD}${line}${RESET}\n`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Dim helper text */
|
|
65
|
+
export function dim(msg: string): void {
|
|
66
|
+
console.log(`${DIM} ${msg}${RESET}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Print a violation detail line */
|
|
70
|
+
export function violation(file: string, line: number | null, rule: string, message: string): void {
|
|
71
|
+
const loc = line !== null ? `:${line}` : '';
|
|
72
|
+
console.log(
|
|
73
|
+
` ${FG.red}✖${RESET} ${FG.white}${file}${loc}${RESET} ${DIM}[${rule}]${RESET} ${FG.yellow}${message}${RESET}`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Print a summary banner */
|
|
78
|
+
export function summary(errors: number, warnings: number): void {
|
|
79
|
+
console.log('');
|
|
80
|
+
if (errors === 0 && warnings === 0) {
|
|
81
|
+
console.log(`${BG.green}${FG.white}${BOLD} ✔ ALL CHECKS PASSED ${RESET}`);
|
|
82
|
+
} else if (errors === 0) {
|
|
83
|
+
console.log(`${BG.yellow}${FG.white}${BOLD} ⚠ ${warnings} WARNING(S) — COMMIT ALLOWED ${RESET}`);
|
|
84
|
+
} else {
|
|
85
|
+
console.log(`${BG.red}${FG.white}${BOLD} ✖ ${errors} ERROR(S), ${warnings} WARNING(S) — COMMIT BLOCKED ${RESET}`);
|
|
86
|
+
}
|
|
87
|
+
console.log('');
|
|
88
|
+
}
|