@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.
Files changed (107) hide show
  1. package/.husky/pre-commit +27 -0
  2. package/LICENSE +21 -0
  3. package/README.md +646 -0
  4. package/config/angular.config.ts +223 -0
  5. package/config/guidelines.config.ts +229 -0
  6. package/config/nextjs.config.ts +160 -0
  7. package/config/react.config.ts +330 -0
  8. package/dist/config/angular.config.d.ts +15 -0
  9. package/dist/config/angular.config.d.ts.map +1 -0
  10. package/dist/config/angular.config.js +187 -0
  11. package/dist/config/angular.config.js.map +1 -0
  12. package/dist/config/guidelines.config.d.ts +63 -0
  13. package/dist/config/guidelines.config.d.ts.map +1 -0
  14. package/dist/config/guidelines.config.js +167 -0
  15. package/dist/config/guidelines.config.js.map +1 -0
  16. package/dist/config/nextjs.config.d.ts +18 -0
  17. package/dist/config/nextjs.config.d.ts.map +1 -0
  18. package/dist/config/nextjs.config.js +133 -0
  19. package/dist/config/nextjs.config.js.map +1 -0
  20. package/dist/config/react.config.d.ts +15 -0
  21. package/dist/config/react.config.d.ts.map +1 -0
  22. package/dist/config/react.config.js +287 -0
  23. package/dist/config/react.config.js.map +1 -0
  24. package/dist/scripts/auto-fix.d.ts +16 -0
  25. package/dist/scripts/auto-fix.d.ts.map +1 -0
  26. package/dist/scripts/auto-fix.js +130 -0
  27. package/dist/scripts/auto-fix.js.map +1 -0
  28. package/dist/scripts/cli.d.ts +17 -0
  29. package/dist/scripts/cli.d.ts.map +1 -0
  30. package/dist/scripts/cli.js +255 -0
  31. package/dist/scripts/cli.js.map +1 -0
  32. package/dist/scripts/delete-bypass-logs.d.ts +17 -0
  33. package/dist/scripts/delete-bypass-logs.d.ts.map +1 -0
  34. package/dist/scripts/delete-bypass-logs.js +242 -0
  35. package/dist/scripts/delete-bypass-logs.js.map +1 -0
  36. package/dist/scripts/generate-doc.d.ts +18 -0
  37. package/dist/scripts/generate-doc.d.ts.map +1 -0
  38. package/dist/scripts/generate-doc.js +300 -0
  39. package/dist/scripts/generate-doc.js.map +1 -0
  40. package/dist/scripts/generate-pr-checklist.d.ts +20 -0
  41. package/dist/scripts/generate-pr-checklist.d.ts.map +1 -0
  42. package/dist/scripts/generate-pr-checklist.js +276 -0
  43. package/dist/scripts/generate-pr-checklist.js.map +1 -0
  44. package/dist/scripts/precommit-check.d.ts +23 -0
  45. package/dist/scripts/precommit-check.d.ts.map +1 -0
  46. package/dist/scripts/precommit-check.js +331 -0
  47. package/dist/scripts/precommit-check.js.map +1 -0
  48. package/dist/scripts/set-admin-password.d.ts +14 -0
  49. package/dist/scripts/set-admin-password.d.ts.map +1 -0
  50. package/dist/scripts/set-admin-password.js +116 -0
  51. package/dist/scripts/set-admin-password.js.map +1 -0
  52. package/dist/scripts/set-bypass-password.d.ts +11 -0
  53. package/dist/scripts/set-bypass-password.d.ts.map +1 -0
  54. package/dist/scripts/set-bypass-password.js +106 -0
  55. package/dist/scripts/set-bypass-password.js.map +1 -0
  56. package/dist/scripts/utils/auto-fixer.d.ts +28 -0
  57. package/dist/scripts/utils/auto-fixer.d.ts.map +1 -0
  58. package/dist/scripts/utils/auto-fixer.js +177 -0
  59. package/dist/scripts/utils/auto-fixer.js.map +1 -0
  60. package/dist/scripts/utils/bypass-manager.d.ts +101 -0
  61. package/dist/scripts/utils/bypass-manager.d.ts.map +1 -0
  62. package/dist/scripts/utils/bypass-manager.js +496 -0
  63. package/dist/scripts/utils/bypass-manager.js.map +1 -0
  64. package/dist/scripts/utils/code-analyzer.d.ts +34 -0
  65. package/dist/scripts/utils/code-analyzer.d.ts.map +1 -0
  66. package/dist/scripts/utils/code-analyzer.js +323 -0
  67. package/dist/scripts/utils/code-analyzer.js.map +1 -0
  68. package/dist/scripts/utils/file-checker.d.ts +93 -0
  69. package/dist/scripts/utils/file-checker.d.ts.map +1 -0
  70. package/dist/scripts/utils/file-checker.js +248 -0
  71. package/dist/scripts/utils/file-checker.js.map +1 -0
  72. package/dist/scripts/utils/logger.d.ts +26 -0
  73. package/dist/scripts/utils/logger.d.ts.map +1 -0
  74. package/dist/scripts/utils/logger.js +86 -0
  75. package/dist/scripts/utils/logger.js.map +1 -0
  76. package/dist/scripts/utils/project-detector.d.ts +34 -0
  77. package/dist/scripts/utils/project-detector.d.ts.map +1 -0
  78. package/dist/scripts/utils/project-detector.js +124 -0
  79. package/dist/scripts/utils/project-detector.js.map +1 -0
  80. package/dist/scripts/utils/rule-engine.d.ts +57 -0
  81. package/dist/scripts/utils/rule-engine.d.ts.map +1 -0
  82. package/dist/scripts/utils/rule-engine.js +158 -0
  83. package/dist/scripts/utils/rule-engine.js.map +1 -0
  84. package/dist/scripts/view-bypass-log.d.ts +13 -0
  85. package/dist/scripts/view-bypass-log.d.ts.map +1 -0
  86. package/dist/scripts/view-bypass-log.js +117 -0
  87. package/dist/scripts/view-bypass-log.js.map +1 -0
  88. package/package.json +74 -0
  89. package/scripts/auto-fix.ts +115 -0
  90. package/scripts/cli.ts +246 -0
  91. package/scripts/delete-bypass-logs.ts +253 -0
  92. package/scripts/generate-doc.ts +317 -0
  93. package/scripts/generate-pr-checklist.ts +285 -0
  94. package/scripts/precommit-check.ts +349 -0
  95. package/scripts/set-admin-password.ts +90 -0
  96. package/scripts/set-bypass-password.ts +80 -0
  97. package/scripts/utils/auto-fixer.ts +181 -0
  98. package/scripts/utils/bypass-manager.ts +566 -0
  99. package/scripts/utils/code-analyzer.ts +341 -0
  100. package/scripts/utils/file-checker.ts +253 -0
  101. package/scripts/utils/logger.ts +88 -0
  102. package/scripts/utils/project-detector.ts +115 -0
  103. package/scripts/utils/rule-engine.ts +186 -0
  104. package/scripts/view-bypass-log.ts +92 -0
  105. package/templates/feature-doc-api.md +101 -0
  106. package/templates/feature-doc-service.md +113 -0
  107. 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
+ }