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