@equilateral_ai/mindmeld 4.0.1 → 4.0.2

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.
@@ -0,0 +1,986 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * MindMeld Repository Analyzer
5
+ *
6
+ * Rich repository analysis for CLI onboarding. Detects:
7
+ * - Technology stack (languages, frameworks, build systems)
8
+ * - Project structure and architecture patterns
9
+ * - Security anti-patterns (linked to remediation standards)
10
+ * - Existing documentation and standards
11
+ *
12
+ * Based on EquilateralAgents analysis patterns.
13
+ */
14
+
15
+ const fs = require('fs').promises;
16
+ const path = require('path');
17
+ const { execSync } = require('child_process');
18
+
19
+ // ============================================================================
20
+ // SECURITY ANTI-PATTERNS (from securityScanner.js)
21
+ // Each links to relevant standards for remediation
22
+ // ============================================================================
23
+
24
+ const SECURITY_PATTERNS = {
25
+ critical: [
26
+ {
27
+ name: 'Hardcoded AWS Credentials',
28
+ pattern: /(?:AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY)\s*=\s*["'][A-Z0-9]{20,}["']/gi,
29
+ description: 'AWS credentials hardcoded in source code',
30
+ recommendation: 'Use environment variables or AWS Secrets Manager',
31
+ cwe: 'CWE-798',
32
+ standardsLink: 'serverless-saas-aws/security/secrets_management.md'
33
+ },
34
+ {
35
+ name: 'Hardcoded API Keys',
36
+ pattern: /(?:api[_-]?key|apikey|api[_-]?secret)\s*=\s*["'][a-zA-Z0-9_\-]{20,}["']/gi,
37
+ description: 'API keys hardcoded in source code',
38
+ recommendation: 'Store API keys in environment variables or secure secret management',
39
+ cwe: 'CWE-798',
40
+ standardsLink: 'serverless-saas-aws/security/secrets_management.md'
41
+ },
42
+ {
43
+ name: 'Hardcoded Private Keys',
44
+ pattern: /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/gi,
45
+ description: 'Private cryptographic key found in source code',
46
+ recommendation: 'Never commit private keys. Use secure key management',
47
+ cwe: 'CWE-798',
48
+ standardsLink: 'serverless-saas-aws/security/secrets_management.md'
49
+ },
50
+ {
51
+ name: 'SQL Injection Risk',
52
+ pattern: /(?:execute|query)\s*\(\s*["'](?:SELECT|INSERT|UPDATE|DELETE)[^"']*["']\s*\+/gi,
53
+ description: 'SQL query using string concatenation',
54
+ recommendation: 'Use parameterized queries or prepared statements',
55
+ cwe: 'CWE-89',
56
+ standardsLink: 'serverless-saas-aws/backend_handler_standards.md'
57
+ },
58
+ {
59
+ name: 'Command Injection Risk',
60
+ pattern: /(?:exec|spawn|system)\s*\([^)]*\$\{[^}]+\}[^)]*\)/gi,
61
+ description: 'Command execution with template literal interpolation',
62
+ recommendation: 'Validate and sanitize all input before command execution',
63
+ cwe: 'CWE-78',
64
+ standardsLink: 'serverless-saas-aws/security/input_validation.md'
65
+ }
66
+ ],
67
+ high: [
68
+ {
69
+ name: 'Hardcoded Passwords',
70
+ pattern: /(?:password|passwd|pwd)\s*=\s*["'][^"']{8,}["']/gi,
71
+ description: 'Password hardcoded in source code',
72
+ recommendation: 'Use environment variables or secure credential management',
73
+ cwe: 'CWE-798',
74
+ standardsLink: 'serverless-saas-aws/security/secrets_management.md'
75
+ },
76
+ {
77
+ name: 'Weak Hash - MD5',
78
+ pattern: /createHash\s*\(\s*["']md5["']\s*\)/gi,
79
+ description: 'MD5 hash algorithm used (cryptographically broken)',
80
+ recommendation: 'Use SHA-256 or stronger for security-critical operations',
81
+ cwe: 'CWE-327',
82
+ standardsLink: 'serverless-saas-aws/security/cryptography.md'
83
+ },
84
+ {
85
+ name: 'Weak Hash - SHA1',
86
+ pattern: /createHash\s*\(\s*["']sha1["']\s*\)/gi,
87
+ description: 'SHA1 hash algorithm used (cryptographically weak)',
88
+ recommendation: 'Use SHA-256 or stronger for security-critical operations',
89
+ cwe: 'CWE-327',
90
+ standardsLink: 'serverless-saas-aws/security/cryptography.md'
91
+ },
92
+ {
93
+ name: 'Insecure Random',
94
+ pattern: /Math\.random\(\)/gi,
95
+ description: 'Math.random() used (not cryptographically secure)',
96
+ recommendation: 'Use crypto.randomBytes() for security-sensitive operations',
97
+ cwe: 'CWE-338',
98
+ standardsLink: 'serverless-saas-aws/security/cryptography.md'
99
+ },
100
+ {
101
+ name: 'Path Traversal Risk',
102
+ pattern: /(?:readFile|writeFile|unlink|rmdir)\s*\([^)]*\.\.[\/\\]/gi,
103
+ description: 'Potential path traversal vulnerability',
104
+ recommendation: 'Validate paths with path.normalize() and check bounds',
105
+ cwe: 'CWE-22',
106
+ standardsLink: 'serverless-saas-aws/security/input_validation.md'
107
+ }
108
+ ],
109
+ medium: [
110
+ {
111
+ name: 'eval() Usage',
112
+ pattern: /\beval\s*\(/gi,
113
+ description: 'eval() can execute arbitrary code',
114
+ recommendation: 'Avoid eval(). Use JSON.parse() for JSON or safer alternatives',
115
+ cwe: 'CWE-95',
116
+ standardsLink: 'serverless-saas-aws/security/input_validation.md'
117
+ },
118
+ {
119
+ name: 'Insecure Deserialization',
120
+ pattern: /(?:unserialize|pickle\.loads|yaml\.load\()/gi,
121
+ description: 'Insecure deserialization pattern',
122
+ recommendation: 'Use safe deserialization methods and validate input',
123
+ cwe: 'CWE-502',
124
+ standardsLink: 'serverless-saas-aws/security/input_validation.md'
125
+ },
126
+ {
127
+ name: 'HTTP instead of HTTPS',
128
+ pattern: /["']http:\/\/(?!localhost|127\.0\.0\.1)/gi,
129
+ description: 'HTTP URL found (unencrypted communication)',
130
+ recommendation: 'Use HTTPS for all external communications',
131
+ cwe: 'CWE-319',
132
+ standardsLink: 'serverless-saas-aws/security/transport.md'
133
+ },
134
+ {
135
+ name: 'Disabled Certificate Validation',
136
+ pattern: /rejectUnauthorized\s*:\s*false/gi,
137
+ description: 'SSL/TLS certificate validation disabled',
138
+ recommendation: 'Enable certificate validation for HTTPS connections',
139
+ cwe: 'CWE-295',
140
+ standardsLink: 'serverless-saas-aws/security/transport.md'
141
+ }
142
+ ],
143
+ low: [
144
+ {
145
+ name: 'Console.log in Production',
146
+ pattern: /console\.(log|debug|info)\(/gi,
147
+ description: 'Console logging may expose sensitive information',
148
+ recommendation: 'Use proper logging frameworks, avoid logging sensitive data',
149
+ cwe: 'CWE-532',
150
+ standardsLink: 'serverless-saas-aws/logging_standards.md'
151
+ },
152
+ {
153
+ name: 'Security TODO Comments',
154
+ pattern: /\/\/\s*(?:TODO|FIXME).*(?:security|password|key|token|auth)/gi,
155
+ description: 'Security-related TODO/FIXME comment found',
156
+ recommendation: 'Address security TODOs before production deployment',
157
+ cwe: 'CWE-1188',
158
+ standardsLink: null
159
+ }
160
+ ]
161
+ };
162
+
163
+ // ============================================================================
164
+ // TECHNOLOGY DETECTION (from PatternHarvestingAgent.js)
165
+ // ============================================================================
166
+
167
+ const TECH_SIGNATURES = {
168
+ // Languages
169
+ javascript: {
170
+ extensions: ['.js', '.mjs', '.cjs'],
171
+ configFiles: ['package.json', 'jsconfig.json'],
172
+ frameworks: {
173
+ react: { indicators: ['react', 'react-dom'], configFiles: [] },
174
+ vue: { indicators: ['vue'], configFiles: ['vue.config.js'] },
175
+ angular: { indicators: ['@angular/core'], configFiles: ['angular.json'] },
176
+ express: { indicators: ['express'], configFiles: [] },
177
+ nest: { indicators: ['@nestjs/core'], configFiles: ['nest-cli.json'] },
178
+ next: { indicators: ['next'], configFiles: ['next.config.js', 'next.config.mjs'] },
179
+ gatsby: { indicators: ['gatsby'], configFiles: ['gatsby-config.js'] }
180
+ }
181
+ },
182
+ typescript: {
183
+ extensions: ['.ts', '.tsx'],
184
+ configFiles: ['tsconfig.json'],
185
+ frameworks: {} // Same as JS
186
+ },
187
+ python: {
188
+ extensions: ['.py'],
189
+ configFiles: ['requirements.txt', 'pyproject.toml', 'setup.py', 'Pipfile'],
190
+ frameworks: {
191
+ django: { indicators: ['django'], configFiles: ['manage.py'] },
192
+ flask: { indicators: ['flask'], configFiles: [] },
193
+ fastapi: { indicators: ['fastapi'], configFiles: [] }
194
+ }
195
+ },
196
+ java: {
197
+ extensions: ['.java'],
198
+ configFiles: ['pom.xml', 'build.gradle', 'build.gradle.kts'],
199
+ frameworks: {
200
+ spring: { indicators: ['springframework', 'spring-boot'], configFiles: [] }
201
+ }
202
+ },
203
+ csharp: {
204
+ extensions: ['.cs'],
205
+ configFiles: ['*.csproj', '*.sln'],
206
+ frameworks: {
207
+ aspnet: { indicators: ['Microsoft.AspNetCore'], configFiles: [] }
208
+ }
209
+ },
210
+ go: {
211
+ extensions: ['.go'],
212
+ configFiles: ['go.mod', 'go.sum'],
213
+ frameworks: {}
214
+ },
215
+ rust: {
216
+ extensions: ['.rs'],
217
+ configFiles: ['Cargo.toml'],
218
+ frameworks: {}
219
+ }
220
+ };
221
+
222
+ // Build systems and infrastructure
223
+ const INFRASTRUCTURE_SIGNATURES = {
224
+ docker: {
225
+ configFiles: ['Dockerfile', 'docker-compose.yml', 'docker-compose.yaml', '.dockerignore']
226
+ },
227
+ kubernetes: {
228
+ configFiles: ['*.yaml', '*.yml'],
229
+ patterns: [/kind:\s*(?:Deployment|Service|Pod|ConfigMap)/]
230
+ },
231
+ terraform: {
232
+ configFiles: ['*.tf', 'terraform.tfvars']
233
+ },
234
+ sam: {
235
+ configFiles: ['template.yaml', 'template.yml', 'samconfig.toml']
236
+ },
237
+ serverless: {
238
+ configFiles: ['serverless.yml', 'serverless.yaml']
239
+ },
240
+ cloudformation: {
241
+ configFiles: ['cloudformation.yaml', 'cloudformation.yml', 'cfn-*.yaml']
242
+ }
243
+ };
244
+
245
+ // ============================================================================
246
+ // PROJECT STRUCTURE PATTERNS (from ProjectStructureDiscoveryAgent.js)
247
+ // ============================================================================
248
+
249
+ const DIRECTORY_PATTERNS = {
250
+ backend: ['src/backend', 'backend', 'server', 'api', 'src/server', 'lambda', 'functions'],
251
+ handlers: ['src/handlers', 'handlers', 'api/handlers', 'lambda/handlers', 'functions'],
252
+ helpers: ['src/helpers', 'helpers', 'utils', 'lib', 'common', 'shared'],
253
+ frontend: ['src/frontend', 'frontend', 'client', 'web', 'ui', 'app', 'src/pages', 'src/components'],
254
+ database: ['database', 'db', 'migrations', 'prisma', 'sql', 'schemas'],
255
+ tests: ['tests', 'test', '__tests__', 'spec', 'e2e', 'integration'],
256
+ deployment: ['deployment', 'deploy', 'infrastructure', '.aws-sam', 'cloudformation', 'terraform', 'k8s'],
257
+ standards: ['.equilateral-standards', '.clinerules', 'standards', '.standards'],
258
+ docs: ['docs', 'documentation', 'doc']
259
+ };
260
+
261
+ // ============================================================================
262
+ // DOCUMENTATION PATTERNS (from LibrarianAgent.js)
263
+ // ============================================================================
264
+
265
+ const DOC_PATTERNS = {
266
+ readme: {
267
+ patterns: [/^readme/i, /^README/],
268
+ description: 'Project README'
269
+ },
270
+ adr: {
271
+ patterns: [/adr[-_]?\d+/i, /decision[-_]record/i, /architecture[-_]decision/i],
272
+ description: 'Architecture Decision Records'
273
+ },
274
+ standards: {
275
+ patterns: [/standards/i, /guidelines/i, /conventions/i, /coding[-_]style/i],
276
+ description: 'Coding standards/guidelines'
277
+ },
278
+ api: {
279
+ patterns: [/^api/i, /swagger/i, /openapi/i],
280
+ description: 'API documentation'
281
+ },
282
+ contributing: {
283
+ patterns: [/contributing/i, /contribution/i],
284
+ description: 'Contribution guidelines'
285
+ },
286
+ changelog: {
287
+ patterns: [/changelog/i, /history/i, /releases/i],
288
+ description: 'Change log'
289
+ }
290
+ };
291
+
292
+ // ============================================================================
293
+ // SCANNABLE FILES CONFIG
294
+ // ============================================================================
295
+
296
+ const SCANNABLE_EXTENSIONS = [
297
+ '.js', '.jsx', '.ts', '.tsx', '.py', '.java', '.go', '.rb', '.php',
298
+ '.cpp', '.c', '.h', '.cs', '.swift', '.kt', '.rs', '.scala'
299
+ ];
300
+
301
+ const SKIP_DIRECTORIES = [
302
+ 'node_modules', '.git', 'dist', 'build', 'coverage', '.next',
303
+ 'target', 'vendor', '__pycache__', '.gradle', 'bin', 'obj',
304
+ '.aws-sam', '.serverless', '.terraform'
305
+ ];
306
+
307
+ // ============================================================================
308
+ // ANALYZER CLASS
309
+ // ============================================================================
310
+
311
+ class RepoAnalyzer {
312
+ constructor(projectPath) {
313
+ this.projectPath = projectPath;
314
+ this.results = {
315
+ timestamp: new Date().toISOString(),
316
+ projectPath,
317
+ projectName: path.basename(projectPath),
318
+ techStack: {
319
+ languages: [],
320
+ frameworks: [],
321
+ buildSystems: [],
322
+ infrastructure: []
323
+ },
324
+ structure: {
325
+ type: 'unknown',
326
+ directories: {},
327
+ hasTests: false,
328
+ hasDocs: false,
329
+ hasStandards: false
330
+ },
331
+ documentation: [],
332
+ securityFindings: {
333
+ critical: [],
334
+ high: [],
335
+ medium: [],
336
+ low: [],
337
+ summary: {}
338
+ },
339
+ recommendations: [],
340
+ contributors: []
341
+ };
342
+ }
343
+
344
+ /**
345
+ * Run full repository analysis
346
+ */
347
+ async analyze(options = {}) {
348
+ const {
349
+ scanSecurity = true,
350
+ maxFiles = 500,
351
+ verbose = false
352
+ } = options;
353
+
354
+ console.log(`\nšŸ” Analyzing repository: ${this.results.projectName}`);
355
+ console.log('─'.repeat(50));
356
+
357
+ try {
358
+ // 1. Detect technology stack
359
+ if (verbose) console.log('\nšŸ“Š Detecting technology stack...');
360
+ await this.detectTechStack();
361
+
362
+ // 2. Analyze project structure
363
+ if (verbose) console.log('šŸ“ Analyzing project structure...');
364
+ await this.analyzeStructure();
365
+
366
+ // 3. Discover documentation
367
+ if (verbose) console.log('šŸ“š Discovering documentation...');
368
+ await this.discoverDocumentation();
369
+
370
+ // 4. Scan for security anti-patterns
371
+ if (scanSecurity) {
372
+ if (verbose) console.log('šŸ”’ Scanning for security anti-patterns...');
373
+ await this.scanSecurity(maxFiles);
374
+ }
375
+
376
+ // 5. Extract contributor profiles from git history
377
+ if (verbose) console.log('šŸ‘„ Extracting contributor profiles...');
378
+ await this.extractContributors();
379
+
380
+ // 6. Generate recommendations
381
+ if (verbose) console.log('šŸ’” Generating recommendations...');
382
+ this.generateRecommendations();
383
+
384
+ return this.results;
385
+ } catch (error) {
386
+ // Preserve full error context for debugging
387
+ this.results.error = {
388
+ message: error.message,
389
+ code: error.code || 'UNKNOWN',
390
+ phase: 'analysis'
391
+ };
392
+ console.error('Analysis error:', error.message);
393
+ return this.results;
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Detect languages, frameworks, and build systems
399
+ */
400
+ async detectTechStack() {
401
+ const files = await this.getAllFiles(this.projectPath, 2); // Shallow scan first
402
+ const fileCounts = {};
403
+ const detectedFrameworks = new Set();
404
+ const detectedInfra = new Set();
405
+
406
+ // Count files by extension
407
+ for (const file of files) {
408
+ const ext = path.extname(file).toLowerCase();
409
+ fileCounts[ext] = (fileCounts[ext] || 0) + 1;
410
+ }
411
+
412
+ // Detect languages
413
+ for (const [language, config] of Object.entries(TECH_SIGNATURES)) {
414
+ const hasExtensions = config.extensions.some(ext => fileCounts[ext] > 0);
415
+ const hasConfig = await this.hasAnyFile(config.configFiles);
416
+
417
+ if (hasExtensions || hasConfig) {
418
+ const count = config.extensions.reduce((sum, ext) => sum + (fileCounts[ext] || 0), 0);
419
+ this.results.techStack.languages.push({
420
+ name: language,
421
+ fileCount: count,
422
+ hasConfig
423
+ });
424
+
425
+ // Check for frameworks
426
+ for (const [framework, fwConfig] of Object.entries(config.frameworks || {})) {
427
+ const hasFramework = await this.detectFramework(framework, fwConfig);
428
+ if (hasFramework) {
429
+ detectedFrameworks.add(framework);
430
+ }
431
+ }
432
+ }
433
+ }
434
+
435
+ // Detect infrastructure
436
+ for (const [infra, config] of Object.entries(INFRASTRUCTURE_SIGNATURES)) {
437
+ const hasInfra = await this.hasAnyFile(config.configFiles);
438
+ if (hasInfra) {
439
+ detectedInfra.add(infra);
440
+ }
441
+ }
442
+
443
+ this.results.techStack.frameworks = Array.from(detectedFrameworks);
444
+ this.results.techStack.infrastructure = Array.from(detectedInfra);
445
+
446
+ // Sort languages by file count
447
+ this.results.techStack.languages.sort((a, b) => b.fileCount - a.fileCount);
448
+ }
449
+
450
+ /**
451
+ * Detect a specific framework
452
+ */
453
+ async detectFramework(name, config) {
454
+ // Check config files
455
+ if (config.configFiles?.length) {
456
+ const hasConfig = await this.hasAnyFile(config.configFiles);
457
+ if (hasConfig) return true;
458
+ }
459
+
460
+ // Check package.json dependencies
461
+ if (config.indicators?.length) {
462
+ const packageJson = await this.readPackageJson();
463
+ if (packageJson) {
464
+ const allDeps = {
465
+ ...(packageJson.dependencies || {}),
466
+ ...(packageJson.devDependencies || {})
467
+ };
468
+ return config.indicators.some(ind => allDeps[ind]);
469
+ }
470
+ }
471
+
472
+ return false;
473
+ }
474
+
475
+ /**
476
+ * Analyze project structure
477
+ */
478
+ async analyzeStructure() {
479
+ const discovered = {};
480
+
481
+ for (const [category, patterns] of Object.entries(DIRECTORY_PATTERNS)) {
482
+ for (const pattern of patterns) {
483
+ const fullPath = path.join(this.projectPath, pattern);
484
+ try {
485
+ const stats = await fs.stat(fullPath);
486
+ if (stats.isDirectory()) {
487
+ if (!discovered[category]) {
488
+ discovered[category] = [];
489
+ }
490
+ discovered[category].push(pattern);
491
+ }
492
+ } catch (error) {
493
+ // Expected: directory doesn't exist (ENOENT) or not accessible (EACCES)
494
+ if (error.code !== 'ENOENT' && error.code !== 'EACCES') {
495
+ console.error(`Unexpected error checking ${pattern}:`, error.message);
496
+ }
497
+ }
498
+ }
499
+ }
500
+
501
+ this.results.structure.directories = discovered;
502
+ this.results.structure.hasTests = !!discovered.tests?.length;
503
+ this.results.structure.hasDocs = !!discovered.docs?.length;
504
+ this.results.structure.hasStandards = !!discovered.standards?.length;
505
+
506
+ // Detect project type
507
+ this.results.structure.type = this.detectProjectType(discovered);
508
+ }
509
+
510
+ /**
511
+ * Detect project type based on structure
512
+ */
513
+ detectProjectType(directories) {
514
+ const hasBackend = !!directories.backend?.length || !!directories.handlers?.length;
515
+ const hasFrontend = !!directories.frontend?.length;
516
+ const hasInfra = this.results.techStack.infrastructure.length > 0;
517
+
518
+ if (hasBackend && hasFrontend) return 'fullstack';
519
+ if (hasBackend && hasInfra) return 'serverless-backend';
520
+ if (hasBackend) return 'backend';
521
+ if (hasFrontend) return 'frontend';
522
+ return 'library';
523
+ }
524
+
525
+ /**
526
+ * Discover existing documentation
527
+ */
528
+ async discoverDocumentation() {
529
+ const docs = [];
530
+
531
+ // Scan root and docs directories
532
+ const dirsToScan = [this.projectPath];
533
+ if (this.results.structure.directories.docs) {
534
+ for (const docDir of this.results.structure.directories.docs) {
535
+ dirsToScan.push(path.join(this.projectPath, docDir));
536
+ }
537
+ }
538
+
539
+ for (const dir of dirsToScan) {
540
+ try {
541
+ const entries = await fs.readdir(dir);
542
+ for (const entry of entries) {
543
+ if (entry.endsWith('.md') || entry.endsWith('.txt')) {
544
+ const docType = this.categorizeDoc(entry);
545
+ if (docType) {
546
+ docs.push({
547
+ path: path.join(dir, entry),
548
+ relativePath: path.relative(this.projectPath, path.join(dir, entry)),
549
+ name: entry,
550
+ type: docType.type,
551
+ description: docType.description
552
+ });
553
+ }
554
+ }
555
+ }
556
+ } catch (error) {
557
+ // Expected: directory doesn't exist or not readable
558
+ if (error.code !== 'ENOENT' && error.code !== 'EACCES') {
559
+ console.error(`Unexpected error scanning ${dir}:`, error.message);
560
+ }
561
+ }
562
+ }
563
+
564
+ this.results.documentation = docs;
565
+ }
566
+
567
+ /**
568
+ * Categorize a documentation file
569
+ */
570
+ categorizeDoc(filename) {
571
+ for (const [type, config] of Object.entries(DOC_PATTERNS)) {
572
+ if (config.patterns.some(p => p.test(filename))) {
573
+ return { type, description: config.description };
574
+ }
575
+ }
576
+ // Generic markdown
577
+ if (filename.endsWith('.md')) {
578
+ return { type: 'other', description: 'Documentation' };
579
+ }
580
+ return null;
581
+ }
582
+
583
+ /**
584
+ * Scan for security anti-patterns
585
+ */
586
+ async scanSecurity(maxFiles = 500) {
587
+ const files = await this.getAllFiles(this.projectPath, 10);
588
+ const scannableFiles = files
589
+ .filter(f => SCANNABLE_EXTENSIONS.includes(path.extname(f).toLowerCase()))
590
+ .slice(0, maxFiles);
591
+
592
+ let scanned = 0;
593
+ for (const file of scannableFiles) {
594
+ try {
595
+ const content = await fs.readFile(file, 'utf8');
596
+ const lines = content.split('\n');
597
+ const relativePath = path.relative(this.projectPath, file);
598
+
599
+ for (const [severity, patterns] of Object.entries(SECURITY_PATTERNS)) {
600
+ for (const pattern of patterns) {
601
+ const regex = new RegExp(pattern.pattern.source, pattern.pattern.flags);
602
+
603
+ lines.forEach((line, lineIndex) => {
604
+ if (regex.test(line)) {
605
+ this.results.securityFindings[severity].push({
606
+ file: relativePath,
607
+ line: lineIndex + 1,
608
+ finding: pattern.name,
609
+ description: pattern.description,
610
+ recommendation: pattern.recommendation,
611
+ cwe: pattern.cwe,
612
+ standardsLink: pattern.standardsLink,
613
+ snippet: line.trim().substring(0, 100)
614
+ });
615
+ }
616
+ });
617
+ }
618
+ }
619
+ scanned++;
620
+ } catch (error) {
621
+ // Expected: file not readable or binary file
622
+ if (error.code !== 'ENOENT' && error.code !== 'EACCES' && !error.message.includes('encoding')) {
623
+ console.error(`Unexpected error scanning ${file}:`, error.message);
624
+ }
625
+ }
626
+ }
627
+
628
+ // Summary
629
+ this.results.securityFindings.summary = {
630
+ filesScanned: scanned,
631
+ critical: this.results.securityFindings.critical.length,
632
+ high: this.results.securityFindings.high.length,
633
+ medium: this.results.securityFindings.medium.length,
634
+ low: this.results.securityFindings.low.length,
635
+ total: this.results.securityFindings.critical.length +
636
+ this.results.securityFindings.high.length +
637
+ this.results.securityFindings.medium.length +
638
+ this.results.securityFindings.low.length
639
+ };
640
+ }
641
+
642
+ /**
643
+ * Extract contributor profiles from git history
644
+ */
645
+ async extractContributors() {
646
+ try {
647
+ execSync('git rev-parse --is-inside-work-tree', { cwd: this.projectPath, stdio: 'pipe' });
648
+ } catch (_) {
649
+ return;
650
+ }
651
+
652
+ try {
653
+ const shortlog = execSync('git shortlog -sne --all', {
654
+ cwd: this.projectPath,
655
+ encoding: 'utf-8',
656
+ timeout: 30000
657
+ }).trim();
658
+
659
+ if (!shortlog) return;
660
+
661
+ const contributors = [];
662
+ for (const line of shortlog.split('\n')) {
663
+ const match = line.trim().match(/^\s*(\d+)\s+(.+?)\s+<(.+?)>\s*$/);
664
+ if (!match) continue;
665
+ contributors.push({
666
+ total_commits: parseInt(match[1], 10),
667
+ name: match[2],
668
+ email: match[3]
669
+ });
670
+ }
671
+
672
+ for (const contributor of contributors.slice(0, 50)) {
673
+ try {
674
+ const firstCommit = execSync(
675
+ `git log --reverse --format=%aI --author="${contributor.email}" | head -1`,
676
+ { cwd: this.projectPath, encoding: 'utf-8', timeout: 10000, shell: true }
677
+ ).trim();
678
+
679
+ const lastCommit = execSync(
680
+ `git log --format=%aI --author="${contributor.email}" -1`,
681
+ { cwd: this.projectPath, encoding: 'utf-8', timeout: 10000 }
682
+ ).trim();
683
+
684
+ contributor.first_commit_date = firstCommit || null;
685
+ contributor.last_commit_date = lastCommit || null;
686
+
687
+ if (firstCommit) {
688
+ const first = new Date(firstCommit);
689
+ const now = new Date();
690
+ contributor.tenure_months = Math.floor(
691
+ (now.getTime() - first.getTime()) / (30.44 * 24 * 3600 * 1000)
692
+ );
693
+ } else {
694
+ contributor.tenure_months = 0;
695
+ }
696
+ } catch (_) {
697
+ contributor.first_commit_date = null;
698
+ contributor.last_commit_date = null;
699
+ contributor.tenure_months = 0;
700
+ }
701
+ }
702
+
703
+ // File ownership — sample up to 200 files
704
+ const ownership = {};
705
+ try {
706
+ const files = execSync('git ls-files | head -200', {
707
+ cwd: this.projectPath,
708
+ encoding: 'utf-8',
709
+ timeout: 10000,
710
+ shell: true
711
+ }).trim().split('\n').filter(Boolean);
712
+
713
+ for (const file of files) {
714
+ try {
715
+ const author = execSync(`git log --format=%ae -1 -- "${file}"`, {
716
+ cwd: this.projectPath,
717
+ encoding: 'utf-8',
718
+ timeout: 5000
719
+ }).trim();
720
+ if (author) {
721
+ ownership[author] = (ownership[author] || 0) + 1;
722
+ }
723
+ } catch (_) { /* skip file */ }
724
+ }
725
+ } catch (_) { /* file ownership optional */ }
726
+
727
+ for (const contributor of contributors.slice(0, 50)) {
728
+ contributor.files_owned = ownership[contributor.email] || 0;
729
+
730
+ if (contributor.total_commits >= 10 && contributor.tenure_months >= 3) {
731
+ contributor.computed_maturity = 'contributor';
732
+ contributor.maturity_basis = `Git bootstrap: ${contributor.total_commits} commits, ${contributor.tenure_months} months tenure`;
733
+ } else {
734
+ contributor.computed_maturity = 'observer';
735
+ contributor.maturity_basis = `${contributor.total_commits} commits, ${contributor.tenure_months} months tenure`;
736
+ }
737
+ }
738
+
739
+ this.results.contributors = contributors.slice(0, 50);
740
+ } catch (err) {
741
+ console.error('Git contributor extraction failed:', err.message);
742
+ }
743
+ }
744
+
745
+ /**
746
+ * Generate recommendations based on analysis
747
+ */
748
+ generateRecommendations() {
749
+ const recs = [];
750
+
751
+ // Tech stack recommendations
752
+ const primaryLang = this.results.techStack.languages[0]?.name;
753
+ if (primaryLang) {
754
+ recs.push({
755
+ category: 'standards',
756
+ priority: 'high',
757
+ title: `${primaryLang} Standards Available`,
758
+ description: `Apply ${primaryLang} coding standards and best practices`,
759
+ standardsPack: `${primaryLang}-standards`
760
+ });
761
+ }
762
+
763
+ // Infrastructure recommendations
764
+ if (this.results.techStack.infrastructure.includes('sam')) {
765
+ recs.push({
766
+ category: 'standards',
767
+ priority: 'high',
768
+ title: 'AWS SAM Standards Available',
769
+ description: 'Apply serverless Lambda handler patterns and SAM template standards',
770
+ standardsPack: 'serverless-saas-aws'
771
+ });
772
+ }
773
+
774
+ // Security recommendations
775
+ const secSummary = this.results.securityFindings.summary;
776
+ if (secSummary.critical > 0 || secSummary.high > 0) {
777
+ recs.push({
778
+ category: 'security',
779
+ priority: 'critical',
780
+ title: 'Security Issues Detected',
781
+ description: `Found ${secSummary.critical} critical and ${secSummary.high} high severity security issues`,
782
+ action: 'Review security findings and apply recommended fixes'
783
+ });
784
+ }
785
+
786
+ // Structure recommendations
787
+ if (!this.results.structure.hasTests) {
788
+ recs.push({
789
+ category: 'quality',
790
+ priority: 'medium',
791
+ title: 'No Test Directory Found',
792
+ description: 'Consider adding automated tests',
793
+ standardsPack: 'testing-standards'
794
+ });
795
+ }
796
+
797
+ if (!this.results.structure.hasStandards && !this.results.structure.hasDocs) {
798
+ recs.push({
799
+ category: 'documentation',
800
+ priority: 'low',
801
+ title: 'No Standards or Documentation Found',
802
+ description: 'Consider documenting coding standards and architecture decisions',
803
+ action: 'MindMeld can inject standards based on your tech stack'
804
+ });
805
+ }
806
+
807
+ this.results.recommendations = recs;
808
+ }
809
+
810
+ // ============================================================================
811
+ // HELPER METHODS
812
+ // ============================================================================
813
+
814
+ /**
815
+ * Get all files in a directory recursively
816
+ */
817
+ async getAllFiles(dirPath, maxDepth = 10, currentDepth = 0) {
818
+ const files = [];
819
+ if (currentDepth >= maxDepth) return files;
820
+
821
+ try {
822
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
823
+
824
+ for (const entry of entries) {
825
+ const fullPath = path.join(dirPath, entry.name);
826
+
827
+ if (entry.isDirectory()) {
828
+ if (!SKIP_DIRECTORIES.includes(entry.name)) {
829
+ files.push(...await this.getAllFiles(fullPath, maxDepth, currentDepth + 1));
830
+ }
831
+ } else {
832
+ files.push(fullPath);
833
+ }
834
+ }
835
+ } catch (error) {
836
+ // Expected: directory not readable or doesn't exist
837
+ if (error.code !== 'ENOENT' && error.code !== 'EACCES') {
838
+ console.error(`Unexpected error reading ${dirPath}:`, error.message);
839
+ }
840
+ }
841
+
842
+ return files;
843
+ }
844
+
845
+ /**
846
+ * Check if any of the given files exist
847
+ */
848
+ async hasAnyFile(patterns) {
849
+ for (const pattern of patterns) {
850
+ if (pattern.includes('*')) {
851
+ // Glob pattern - simplified check
852
+ const dir = path.dirname(pattern) || '.';
853
+ const ext = path.extname(pattern);
854
+ try {
855
+ const entries = await fs.readdir(path.join(this.projectPath, dir));
856
+ if (entries.some(e => e.endsWith(ext))) return true;
857
+ } catch (error) {
858
+ // Expected: directory doesn't exist
859
+ if (error.code !== 'ENOENT' && error.code !== 'EACCES') {
860
+ console.error(`Unexpected error checking ${dir}:`, error.message);
861
+ }
862
+ }
863
+ } else {
864
+ try {
865
+ await fs.access(path.join(this.projectPath, pattern));
866
+ return true;
867
+ } catch (error) {
868
+ // Expected: file doesn't exist
869
+ if (error.code !== 'ENOENT' && error.code !== 'EACCES') {
870
+ console.error(`Unexpected error checking ${pattern}:`, error.message);
871
+ }
872
+ }
873
+ }
874
+ }
875
+ return false;
876
+ }
877
+
878
+ /**
879
+ * Read package.json if it exists
880
+ */
881
+ async readPackageJson() {
882
+ try {
883
+ const content = await fs.readFile(path.join(this.projectPath, 'package.json'), 'utf8');
884
+ return JSON.parse(content);
885
+ } catch (error) {
886
+ // Expected: package.json doesn't exist or is invalid JSON
887
+ if (error.code !== 'ENOENT' && !(error instanceof SyntaxError)) {
888
+ console.error('Unexpected error reading package.json:', error.message);
889
+ }
890
+ return null;
891
+ }
892
+ }
893
+
894
+ /**
895
+ * Generate a human-readable summary
896
+ */
897
+ getSummary() {
898
+ const r = this.results;
899
+ const lines = [];
900
+
901
+ lines.push(`\nšŸ“¦ Project: ${r.projectName}`);
902
+ lines.push(` Type: ${r.structure.type}`);
903
+
904
+ if (r.techStack.languages.length) {
905
+ const langs = r.techStack.languages.map(l => `${l.name}(${l.fileCount})`).join(', ');
906
+ lines.push(`\nšŸ”§ Languages: ${langs}`);
907
+ }
908
+
909
+ if (r.techStack.frameworks.length) {
910
+ lines.push(` Frameworks: ${r.techStack.frameworks.join(', ')}`);
911
+ }
912
+
913
+ if (r.techStack.infrastructure.length) {
914
+ lines.push(` Infrastructure: ${r.techStack.infrastructure.join(', ')}`);
915
+ }
916
+
917
+ if (r.documentation.length) {
918
+ lines.push(`\nšŸ“š Documentation found: ${r.documentation.length} files`);
919
+ for (const doc of r.documentation.slice(0, 5)) {
920
+ lines.push(` - ${doc.relativePath} (${doc.description})`);
921
+ }
922
+ }
923
+
924
+ const sec = r.securityFindings.summary;
925
+ if (sec.total > 0) {
926
+ lines.push(`\nšŸ”’ Security findings:`);
927
+ if (sec.critical) lines.push(` ā›” Critical: ${sec.critical}`);
928
+ if (sec.high) lines.push(` šŸ”“ High: ${sec.high}`);
929
+ if (sec.medium) lines.push(` 🟔 Medium: ${sec.medium}`);
930
+ if (sec.low) lines.push(` 🟢 Low: ${sec.low}`);
931
+ } else if (sec.filesScanned > 0) {
932
+ lines.push(`\nāœ… No security issues found in ${sec.filesScanned} files scanned`);
933
+ }
934
+
935
+ if (r.contributors && r.contributors.length > 0) {
936
+ lines.push(`\nšŸ‘„ Contributors: ${r.contributors.length} found`);
937
+ for (const c of r.contributors.slice(0, 10)) {
938
+ lines.push(` - ${c.name} <${c.email}> (${c.total_commits} commits, ${c.tenure_months}mo, ${c.computed_maturity})`);
939
+ }
940
+ }
941
+
942
+ if (r.recommendations.length) {
943
+ lines.push(`\nšŸ’” Recommendations:`);
944
+ for (const rec of r.recommendations) {
945
+ const icon = rec.priority === 'critical' ? 'ā›”' : rec.priority === 'high' ? 'šŸ”¶' : 'šŸ’”';
946
+ lines.push(` ${icon} ${rec.title}`);
947
+ }
948
+ }
949
+
950
+ return lines.join('\n');
951
+ }
952
+ }
953
+
954
+ // ============================================================================
955
+ // CLI ENTRY POINT
956
+ // ============================================================================
957
+
958
+ async function main() {
959
+ const args = process.argv.slice(2);
960
+ const verbose = args.includes('--verbose') || args.includes('-v');
961
+ const json = args.includes('--json');
962
+
963
+ // Filter out flags to get the path
964
+ const pathArg = args.find(a => !a.startsWith('-'));
965
+ const projectPath = pathArg || process.cwd();
966
+
967
+ const analyzer = new RepoAnalyzer(projectPath);
968
+ const results = await analyzer.analyze({ verbose });
969
+
970
+ if (json) {
971
+ console.log(JSON.stringify(results, null, 2));
972
+ } else {
973
+ console.log(analyzer.getSummary());
974
+ }
975
+ }
976
+
977
+ // Export for use as module
978
+ module.exports = { RepoAnalyzer, SECURITY_PATTERNS, TECH_SIGNATURES, DIRECTORY_PATTERNS };
979
+
980
+ // Run if called directly
981
+ if (require.main === module) {
982
+ main().catch(error => {
983
+ console.error('Fatal error:', error.message);
984
+ process.exit(1);
985
+ });
986
+ }